First version AI Search
This commit is contained in:
parent
b7b34dacab
commit
af982d19d8
|
|
@ -19,13 +19,15 @@
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Debug Current TS File",
|
"name": "Launch TypeScript file with tsx",
|
||||||
"program": "${workspaceFolder}/dist/src/drizzle/${fileBasenameNoExtension}.js",
|
"runtimeExecutable": "npx",
|
||||||
"preLaunchTask": "tsc: build - tsconfig.json",
|
"runtimeArgs": ["tsx", "--inspect"],
|
||||||
"outFiles": ["${workspaceFolder}/out/**/*.js"],
|
"args": ["${workspaceFolder}/src/drizzle/import.ts"],
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"outFiles": ["${workspaceFolder}/dist/**/*.js", "!**/node_modules/**"],
|
||||||
"sourceMaps": true,
|
"sourceMaps": true,
|
||||||
"smartStep": true,
|
"resolveSourceMapLocations": ["${workspaceFolder}/src/**/*.ts", "!**/node_modules/**"],
|
||||||
"internalConsoleOptions": "openOnSessionStart"
|
"skipFiles": ["<node_internals>/**", "${workspaceFolder}/node_modules/**/*.js"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
"reflect-metadata": "^0.2.0",
|
"reflect-metadata": "^0.2.0",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
"sharp": "^0.33.2",
|
"sharp": "^0.33.2",
|
||||||
"tsx": "^4.7.2",
|
"tsx": "^4.16.2",
|
||||||
"urlcat": "^3.1.0",
|
"urlcat": "^3.1.0",
|
||||||
"winston": "^3.11.0"
|
"winston": "^3.11.0"
|
||||||
},
|
},
|
||||||
|
|
@ -78,6 +78,7 @@
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^6.0.0",
|
||||||
"commander": "^12.0.0",
|
"commander": "^12.0.0",
|
||||||
"drizzle-kit": "^0.23.0",
|
"drizzle-kit": "^0.23.0",
|
||||||
|
"esbuild-register": "^3.5.0",
|
||||||
"eslint": "^8.42.0",
|
"eslint": "^8.42.0",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^9.0.0",
|
||||||
"eslint-plugin-prettier": "^5.0.0",
|
"eslint-plugin-prettier": "^5.0.0",
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ import { join } from 'path';
|
||||||
import pkg from 'pg';
|
import pkg from 'pg';
|
||||||
import { rimraf } from 'rimraf';
|
import { rimraf } from 'rimraf';
|
||||||
import sharp from 'sharp';
|
import sharp from 'sharp';
|
||||||
|
import { SelectOptionsService } from 'src/select-options/select-options.service.js';
|
||||||
import winston from 'winston';
|
import winston from 'winston';
|
||||||
import { BusinessListing, CommercialPropertyListing, User, UserData } from '../models/db.model.js';
|
import { BusinessListing, CommercialPropertyListing, User, UserData } from '../models/db.model.js';
|
||||||
import { emailToDirName, KeyValueStyle } from '../models/main.model.js';
|
import { emailToDirName, KeyValueStyle } from '../models/main.model.js';
|
||||||
import * as schema from './schema.js';
|
import * as schema from './schema.js';
|
||||||
|
import { users } from './schema.js';
|
||||||
const typesOfBusiness: Array<KeyValueStyle> = [
|
const typesOfBusiness: Array<KeyValueStyle> = [
|
||||||
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
|
{ name: 'Automotive', value: '1', icon: 'fa-solid fa-car', textColorClass: 'text-green-400' },
|
||||||
{ name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
|
{ name: 'Industrial Services', value: '2', icon: 'fa-solid fa-industry', textColorClass: 'text-yellow-400' },
|
||||||
|
|
@ -44,6 +46,7 @@ await db.delete(schema.commercials);
|
||||||
await db.delete(schema.businesses);
|
await db.delete(schema.businesses);
|
||||||
await db.delete(schema.users);
|
await db.delete(schema.users);
|
||||||
|
|
||||||
|
const sso = new SelectOptionsService();
|
||||||
//Broker
|
//Broker
|
||||||
let filePath = `./data/broker.json`;
|
let filePath = `./data/broker.json`;
|
||||||
let data: string = readFileSync(filePath, 'utf8');
|
let data: string = readFileSync(filePath, 'utf8');
|
||||||
|
|
@ -63,6 +66,8 @@ fs.ensureDirSync(`./pictures/logo`);
|
||||||
fs.ensureDirSync(`./pictures/profile`);
|
fs.ensureDirSync(`./pictures/profile`);
|
||||||
fs.ensureDirSync(`./pictures/property`);
|
fs.ensureDirSync(`./pictures/property`);
|
||||||
type UserProfile = Omit<User, 'created' | 'updated' | 'hasCompanyLogo' | 'hasProfile' | 'id'>;
|
type UserProfile = Omit<User, 'created' | 'updated' | 'hasCompanyLogo' | 'hasProfile' | 'id'>;
|
||||||
|
|
||||||
|
type NewUser = typeof users.$inferInsert;
|
||||||
//for (const userData of usersData) {
|
//for (const userData of usersData) {
|
||||||
for (let index = 0; index < usersData.length; index++) {
|
for (let index = 0; index < usersData.length; index++) {
|
||||||
const userData = usersData[index];
|
const userData = usersData[index];
|
||||||
|
|
@ -100,18 +105,17 @@ for (let index = 0; index < usersData.length; index++) {
|
||||||
const userProfile = createUserProfile(user);
|
const userProfile = createUserProfile(user);
|
||||||
logger.info(`${index} - ${JSON.stringify(userProfile)}`);
|
logger.info(`${index} - ${JSON.stringify(userProfile)}`);
|
||||||
const embedding = await createEmbedding(JSON.stringify(userProfile));
|
const embedding = await createEmbedding(JSON.stringify(userProfile));
|
||||||
sleep(500);
|
sleep(200);
|
||||||
const u = await db
|
const u = await db
|
||||||
.insert(schema.users)
|
.insert(schema.users)
|
||||||
.values({
|
.values({
|
||||||
...user,
|
...user,
|
||||||
embedding: embedding,
|
embedding: embedding,
|
||||||
})
|
} as NewUser)
|
||||||
.returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email });
|
.returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email, firstname: schema.users.firstname, lastname: schema.users.lastname });
|
||||||
// const u = await db.insert(schema.users).values(user).returning({ insertedId: schema.users.id, gender: schema.users.gender, email: schema.users.email });
|
|
||||||
generatedUserData.push(u[0]);
|
generatedUserData.push(u[0]);
|
||||||
i++;
|
i++;
|
||||||
|
logger.info(`user_${index} inserted`);
|
||||||
if (u[0].gender === 'male') {
|
if (u[0].gender === 'male') {
|
||||||
male++;
|
male++;
|
||||||
const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`);
|
const data = readFileSync(`./pictures_base/profile/Mann_${male}.jpg`);
|
||||||
|
|
@ -125,46 +129,12 @@ for (let index = 0; index < usersData.length; index++) {
|
||||||
await storeCompanyLogo(data, emailToDirName(u[0].email));
|
await storeCompanyLogo(data, emailToDirName(u[0].email));
|
||||||
}
|
}
|
||||||
|
|
||||||
//Business Listings
|
|
||||||
filePath = `./data/businesses.json`;
|
|
||||||
data = readFileSync(filePath, 'utf8');
|
|
||||||
const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein Array von Objekten
|
|
||||||
for (const business of businessJsonData) {
|
|
||||||
delete business.id;
|
|
||||||
business.created = new Date(business.created);
|
|
||||||
business.updated = new Date(business.created);
|
|
||||||
const user = getRandomItem(generatedUserData);
|
|
||||||
business.email = user.email;
|
|
||||||
business.imageName = emailToDirName(user.email);
|
|
||||||
const embeddingText = JSON.stringify({
|
|
||||||
type: typesOfBusiness.find(b => b.value === String(business.type))?.name,
|
|
||||||
title: business.title,
|
|
||||||
description: business.description,
|
|
||||||
city: business.city,
|
|
||||||
state: business.state,
|
|
||||||
price: business.price,
|
|
||||||
realEstateIncluded: business.realEstateIncluded,
|
|
||||||
leasedLocation: business.leasedLocation,
|
|
||||||
franchiseResale: business.franchiseResale,
|
|
||||||
salesRevenue: business.salesRevenue,
|
|
||||||
cashFlow: business.cashFlow,
|
|
||||||
supportAndTraining: business.supportAndTraining,
|
|
||||||
employees: business.employees,
|
|
||||||
established: business.established,
|
|
||||||
reasonForSale: business.reasonForSale,
|
|
||||||
});
|
|
||||||
const embedding = await createEmbedding(embeddingText);
|
|
||||||
sleep(300);
|
|
||||||
await db.insert(schema.businesses).values({
|
|
||||||
...business,
|
|
||||||
embedding: embedding,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//Corporate Listings
|
//Corporate Listings
|
||||||
filePath = `./data/commercials.json`;
|
filePath = `./data/commercials.json`;
|
||||||
data = readFileSync(filePath, 'utf8');
|
data = readFileSync(filePath, 'utf8');
|
||||||
const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // Erwartet ein Array von Objekten
|
const commercialJsonData = JSON.parse(data) as CommercialPropertyListing[]; // Erwartet ein Array von Objekten
|
||||||
for (const commercial of commercialJsonData) {
|
for (let index = 0; index < commercialJsonData.length; index++) {
|
||||||
|
const commercial = commercialJsonData[index];
|
||||||
const id = commercial.id;
|
const id = commercial.id;
|
||||||
delete commercial.id;
|
delete commercial.id;
|
||||||
const user = getRandomItem(generatedUserData);
|
const user = getRandomItem(generatedUserData);
|
||||||
|
|
@ -175,7 +145,25 @@ for (const commercial of commercialJsonData) {
|
||||||
commercial.updated = insertionDate;
|
commercial.updated = insertionDate;
|
||||||
commercial.email = user.email;
|
commercial.email = user.email;
|
||||||
commercial.draft = false;
|
commercial.draft = false;
|
||||||
const result = await db.insert(schema.commercials).values(commercial).returning();
|
const reducedCommercial = {
|
||||||
|
city: commercial.city,
|
||||||
|
description: commercial.description,
|
||||||
|
email: commercial.email,
|
||||||
|
price: commercial.price,
|
||||||
|
state: sso.locations.find(l => l.value === commercial.state)?.name,
|
||||||
|
title: commercial.title,
|
||||||
|
name: `${user.firstname} ${user.lastname}`,
|
||||||
|
};
|
||||||
|
const embedding = await createEmbedding(JSON.stringify(reducedCommercial));
|
||||||
|
sleep(200);
|
||||||
|
const result = await db
|
||||||
|
.insert(schema.commercials)
|
||||||
|
.values({
|
||||||
|
...commercial,
|
||||||
|
embedding: embedding,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
logger.info(`commercial_${index} inserted`);
|
||||||
try {
|
try {
|
||||||
fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result[0].imagePath}/${result[0].serialId}`);
|
fs.copySync(`./pictures_base/property/${id}`, `./pictures/property/${result[0].imagePath}/${result[0].serialId}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -183,6 +171,46 @@ for (const commercial of commercialJsonData) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Business Listings
|
||||||
|
filePath = `./data/businesses.json`;
|
||||||
|
data = readFileSync(filePath, 'utf8');
|
||||||
|
const businessJsonData = JSON.parse(data) as BusinessListing[]; // Erwartet ein Array von Objekten
|
||||||
|
for (let index = 0; index < businessJsonData.length; index++) {
|
||||||
|
const business = businessJsonData[index];
|
||||||
|
delete business.id;
|
||||||
|
business.created = new Date(business.created);
|
||||||
|
business.updated = new Date(business.created);
|
||||||
|
const user = getRandomItem(generatedUserData);
|
||||||
|
business.email = user.email;
|
||||||
|
business.imageName = emailToDirName(user.email);
|
||||||
|
const embeddingText = JSON.stringify({
|
||||||
|
type: typesOfBusiness.find(b => b.value === String(business.type))?.name,
|
||||||
|
title: business.title,
|
||||||
|
description: business.description,
|
||||||
|
email: business.email,
|
||||||
|
city: business.city,
|
||||||
|
state: sso.locations.find(l => l.value === business.state)?.name,
|
||||||
|
price: business.price,
|
||||||
|
realEstateIncluded: business.realEstateIncluded,
|
||||||
|
leasedLocation: business.leasedLocation,
|
||||||
|
franchiseResale: business.franchiseResale,
|
||||||
|
salesRevenue: business.salesRevenue,
|
||||||
|
cashFlow: business.cashFlow,
|
||||||
|
supportAndTraining: business.supportAndTraining,
|
||||||
|
employees: business.employees,
|
||||||
|
established: business.established,
|
||||||
|
reasonForSale: business.reasonForSale,
|
||||||
|
name: `${user.firstname} ${user.lastname}`,
|
||||||
|
});
|
||||||
|
const embedding = await createEmbedding(embeddingText);
|
||||||
|
sleep(200);
|
||||||
|
await db.insert(schema.businesses).values({
|
||||||
|
...business,
|
||||||
|
embedding: embedding,
|
||||||
|
});
|
||||||
|
logger.info(`business_${index} inserted`);
|
||||||
|
}
|
||||||
|
|
||||||
//End
|
//End
|
||||||
await client.end();
|
await client.end();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,17 @@ export class BusinessListingsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(OptionalJwtAuthGuard)
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Post('search')
|
@Post('find')
|
||||||
find(@Request() req, @Body() criteria: ListingCriteria): any {
|
find(@Request() req, @Body() criteria: ListingCriteria): any {
|
||||||
return this.listingsService.findBusinessListings(criteria, req.user as JwtUser);
|
return this.listingsService.findBusinessListings(criteria, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
|
@Post('search')
|
||||||
|
search(@Request() req, @Body() criteria: ListingCriteria): any {
|
||||||
|
return this.listingsService.searchBusinessListings(criteria.prompt);
|
||||||
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
create(@Body() listing: any) {
|
create(@Body() listing: any) {
|
||||||
this.logger.info(`Save Listing`);
|
this.logger.info(`Save Listing`);
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ export class CommercialPropertyListingsController {
|
||||||
return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
|
return this.listingsService.findCommercialPropertiesByEmail(email, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
@UseGuards(OptionalJwtAuthGuard)
|
@UseGuards(OptionalJwtAuthGuard)
|
||||||
@Post('search')
|
@Post('find')
|
||||||
async find(@Request() req, @Body() criteria: ListingCriteria): Promise<any> {
|
async find(@Request() req, @Body() criteria: ListingCriteria): Promise<any> {
|
||||||
return await this.listingsService.findCommercialPropertyListings(criteria, req.user as JwtUser);
|
return await this.listingsService.findCommercialPropertyListings(criteria, req.user as JwtUser);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { and, eq, gte, ilike, lte, ne, or, sql } from 'drizzle-orm';
|
import { and, eq, gte, ilike, lte, ne, or, sql } from 'drizzle-orm';
|
||||||
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
import { NodePgDatabase } from 'drizzle-orm/node-postgres';
|
||||||
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
|
||||||
|
import OpenAI from 'openai';
|
||||||
import { Logger } from 'winston';
|
import { Logger } from 'winston';
|
||||||
import * as schema from '../drizzle/schema.js';
|
import * as schema from '../drizzle/schema.js';
|
||||||
import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js';
|
import { PG_CONNECTION, businesses, commercials } from '../drizzle/schema.js';
|
||||||
|
|
@ -11,11 +12,16 @@ import { JwtUser, ListingCriteria, emailToDirName } from '../models/main.model.j
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ListingsService {
|
export class ListingsService {
|
||||||
|
openai: OpenAI;
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
|
||||||
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
@Inject(PG_CONNECTION) private conn: NodePgDatabase<typeof schema>,
|
||||||
private fileService: FileService,
|
private fileService: FileService,
|
||||||
) {}
|
) {
|
||||||
|
this.openai = new OpenAI({
|
||||||
|
apiKey: process.env.OPENAI_API_KEY, // Stellen Sie sicher, dass Sie Ihren API-Key als Umgebungsvariable setzen
|
||||||
|
});
|
||||||
|
}
|
||||||
private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials, user: JwtUser): any[] {
|
private getConditions(criteria: ListingCriteria, table: typeof businesses | typeof commercials, user: JwtUser): any[] {
|
||||||
const conditions = [];
|
const conditions = [];
|
||||||
if (criteria.type) {
|
if (criteria.type) {
|
||||||
|
|
@ -42,6 +48,19 @@ export class ListingsService {
|
||||||
// Listings general
|
// Listings general
|
||||||
// ##############################################################
|
// ##############################################################
|
||||||
|
|
||||||
|
// #### Find by embeddng ########################################
|
||||||
|
async searchBusinessListings(query: string, limit: number = 20): Promise<BusinessListing[]> {
|
||||||
|
const queryEmbedding = await this.createEmbedding(query);
|
||||||
|
|
||||||
|
const results = await this.conn
|
||||||
|
.select()
|
||||||
|
.from(businesses)
|
||||||
|
.orderBy(sql`embedding <-> ${JSON.stringify(queryEmbedding)}`)
|
||||||
|
.limit(limit);
|
||||||
|
|
||||||
|
return results as BusinessListing[];
|
||||||
|
}
|
||||||
|
// #### Find by criteria ########################################
|
||||||
async findCommercialPropertyListings(criteria: ListingCriteria, user: JwtUser): Promise<any> {
|
async findCommercialPropertyListings(criteria: ListingCriteria, user: JwtUser): Promise<any> {
|
||||||
const start = criteria.start ? criteria.start : 0;
|
const start = criteria.start ? criteria.start : 0;
|
||||||
const length = criteria.length ? criteria.length : 12;
|
const length = criteria.length ? criteria.length : 12;
|
||||||
|
|
@ -86,6 +105,8 @@ export class ListingsService {
|
||||||
]);
|
]);
|
||||||
return { total, data };
|
return { total, data };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #### Find by ID ########################################
|
||||||
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
|
async findCommercialPropertiesById(id: string, user: JwtUser): Promise<CommercialPropertyListing> {
|
||||||
let result = await this.conn
|
let result = await this.conn
|
||||||
.select()
|
.select()
|
||||||
|
|
@ -102,6 +123,8 @@ export class ListingsService {
|
||||||
result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
|
result = result.filter(r => !r.draft || r.imageName === emailToDirName(user?.username) || user?.roles.includes('ADMIN'));
|
||||||
return result[0] as BusinessListing;
|
return result[0] as BusinessListing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #### Find by User EMail ########################################
|
||||||
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
|
async findCommercialPropertiesByEmail(email: string, user: JwtUser): Promise<CommercialPropertyListing[]> {
|
||||||
const conditions = [];
|
const conditions = [];
|
||||||
conditions.push(eq(commercials.imagePath, emailToDirName(email)));
|
conditions.push(eq(commercials.imagePath, emailToDirName(email)));
|
||||||
|
|
@ -124,6 +147,8 @@ export class ListingsService {
|
||||||
.from(businesses)
|
.from(businesses)
|
||||||
.where(and(...conditions))) as CommercialPropertyListing[];
|
.where(and(...conditions))) as CommercialPropertyListing[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #### Find by imagePath ########################################
|
||||||
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
|
async findByImagePath(imagePath: string, serial: string): Promise<CommercialPropertyListing> {
|
||||||
const result = await this.conn
|
const result = await this.conn
|
||||||
.select()
|
.select()
|
||||||
|
|
@ -131,13 +156,15 @@ export class ListingsService {
|
||||||
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
|
.where(and(sql`${commercials.imagePath} = ${imagePath}`, sql`${commercials.serialId} = ${serial}`));
|
||||||
return result[0] as CommercialPropertyListing;
|
return result[0] as CommercialPropertyListing;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #### CREATE ########################################
|
||||||
async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
async createListing(data: BusinessListing | CommercialPropertyListing, table: typeof businesses | typeof commercials): Promise<BusinessListing | CommercialPropertyListing> {
|
||||||
data.created = new Date();
|
data.created = new Date();
|
||||||
data.updated = new Date();
|
data.updated = new Date();
|
||||||
const [createdListing] = await this.conn.insert(table).values(data).returning();
|
const [createdListing] = await this.conn.insert(table).values(data).returning();
|
||||||
return createdListing as BusinessListing | CommercialPropertyListing;
|
return createdListing as BusinessListing | CommercialPropertyListing;
|
||||||
}
|
}
|
||||||
|
// #### UPDATE CommercialProps ########################################
|
||||||
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<BusinessListing | CommercialPropertyListing> {
|
async updateCommercialPropertyListing(id: string, data: CommercialPropertyListing): Promise<BusinessListing | CommercialPropertyListing> {
|
||||||
data.updated = new Date();
|
data.updated = new Date();
|
||||||
data.created = new Date(data.created);
|
data.created = new Date(data.created);
|
||||||
|
|
@ -150,22 +177,18 @@ export class ListingsService {
|
||||||
const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning();
|
const [updateListing] = await this.conn.update(commercials).set(data).where(eq(commercials.id, id)).returning();
|
||||||
return updateListing as BusinessListing | CommercialPropertyListing;
|
return updateListing as BusinessListing | CommercialPropertyListing;
|
||||||
}
|
}
|
||||||
|
// #### UPDATE Business ########################################
|
||||||
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing | CommercialPropertyListing> {
|
async updateBusinessListing(id: string, data: BusinessListing): Promise<BusinessListing | CommercialPropertyListing> {
|
||||||
data.updated = new Date();
|
data.updated = new Date();
|
||||||
data.created = new Date(data.created);
|
data.created = new Date(data.created);
|
||||||
const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning();
|
const [updateListing] = await this.conn.update(businesses).set(data).where(eq(businesses.id, id)).returning();
|
||||||
return updateListing as BusinessListing | CommercialPropertyListing;
|
return updateListing as BusinessListing | CommercialPropertyListing;
|
||||||
}
|
}
|
||||||
|
// #### DELETE ########################################
|
||||||
async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
|
async deleteListing(id: string, table: typeof businesses | typeof commercials): Promise<void> {
|
||||||
await this.conn.delete(table).where(eq(table.id, id));
|
await this.conn.delete(table).where(eq(table.id, id));
|
||||||
}
|
}
|
||||||
async getStates(table: typeof businesses | typeof commercials): Promise<any[]> {
|
|
||||||
return await this.conn
|
|
||||||
.select({ state: table.state, count: sql<number>`count(${table.id})`.mapWith(Number) })
|
|
||||||
.from(table)
|
|
||||||
.groupBy(sql`${table.state}`)
|
|
||||||
.orderBy(sql`count desc`);
|
|
||||||
}
|
|
||||||
// ##############################################################
|
// ##############################################################
|
||||||
// Images for commercial Properties
|
// Images for commercial Properties
|
||||||
// ##############################################################
|
// ##############################################################
|
||||||
|
|
@ -182,4 +205,24 @@ export class ListingsService {
|
||||||
listing.imageOrder.push(imagename);
|
listing.imageOrder.push(imagename);
|
||||||
await this.updateCommercialPropertyListing(listing.id, listing);
|
await this.updateCommercialPropertyListing(listing.id, listing);
|
||||||
}
|
}
|
||||||
|
// ##############################################################
|
||||||
|
// States
|
||||||
|
// ##############################################################
|
||||||
|
async getStates(table: typeof businesses | typeof commercials): Promise<any[]> {
|
||||||
|
return await this.conn
|
||||||
|
.select({ state: table.state, count: sql<number>`count(${table.id})`.mapWith(Number) })
|
||||||
|
.from(table)
|
||||||
|
.groupBy(sql`${table.state}`)
|
||||||
|
.orderBy(sql`count desc`);
|
||||||
|
}
|
||||||
|
// ##############################################################
|
||||||
|
// Embedding
|
||||||
|
// ##############################################################
|
||||||
|
async createEmbedding(text: string): Promise<number[]> {
|
||||||
|
const response = await this.openai.embeddings.create({
|
||||||
|
model: 'text-embedding-3-small',
|
||||||
|
input: text,
|
||||||
|
});
|
||||||
|
return response.data[0].embedding;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ export interface ListingCriteria {
|
||||||
title: string;
|
title: string;
|
||||||
category: 'professional' | 'broker';
|
category: 'professional' | 'broker';
|
||||||
name: string;
|
name: string;
|
||||||
|
prompt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeycloakUser {
|
export interface KeycloakUser {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component, HostListener } from '@angular/core';
|
import { Component, HostListener } from '@angular/core';
|
||||||
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
|
import { ActivatedRoute, NavigationEnd, Router, RouterOutlet } from '@angular/router';
|
||||||
import { initFlowbite } from 'flowbite';
|
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import onChange from 'on-change';
|
import onChange from 'on-change';
|
||||||
|
|
||||||
|
|
@ -42,7 +41,11 @@ export class AppComponent {
|
||||||
ngOnInit() {}
|
ngOnInit() {}
|
||||||
@HostListener('window:keydown', ['$event'])
|
@HostListener('window:keydown', ['$event'])
|
||||||
handleKeyboardEvent(event: KeyboardEvent) {
|
handleKeyboardEvent(event: KeyboardEvent) {
|
||||||
initFlowbite();
|
// this.router.events.subscribe(event => {
|
||||||
|
// if (event instanceof NavigationEnd) {
|
||||||
|
// initFlowbite();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
if (event.shiftKey && event.ctrlKey && event.key === 'V') {
|
if (event.shiftKey && event.ctrlKey && event.key === 'V') {
|
||||||
this.showVersionDialog();
|
this.showVersionDialog();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<p>dropdown works!</p>
|
||||||
|
|
@ -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 { CommonModule } from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { RouterModule } from '@angular/router';
|
import { NavigationEnd, Router, RouterModule } from '@angular/router';
|
||||||
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
|
||||||
import { initFlowbite } from 'flowbite';
|
import { initFlowbite } from 'flowbite';
|
||||||
@Component({
|
@Component({
|
||||||
|
|
@ -15,7 +15,12 @@ export class FooterComponent {
|
||||||
privacyVisible = false;
|
privacyVisible = false;
|
||||||
termsVisible = false;
|
termsVisible = false;
|
||||||
currentYear: number = new Date().getFullYear();
|
currentYear: number = new Date().getFullYear();
|
||||||
|
constructor(private router: Router) {}
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
this.router.events.subscribe(event => {
|
||||||
|
if (event instanceof NavigationEnd) {
|
||||||
initFlowbite();
|
initFlowbite();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,114 @@
|
||||||
<!-- <div class="wrapper">
|
<!-- <nav class="bg-white border-gray-200 dark:bg-gray-900">
|
||||||
<div class="pl-3 flex align-items-center gap-2">
|
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
||||||
<a routerLink="/home"><img src="assets/images/header-logo.png" height="40" alt="bizmatch" /></a>
|
<a routerLink="/home" class="flex items-center space-x-3 rtl:space-x-reverse">
|
||||||
<p-tabMenu [model]="tabItems" ariaLabelledBy="label" styleClass="flex" [activeItem]="activeItem"> </p-tabMenu>
|
<img src="assets/images/header-logo.png" class="h-8" alt="Flowbite Logo" />
|
||||||
<p-menubar [model]="menuItems"></p-menubar>
|
</a>
|
||||||
<p-menubar [model]="loginItems"></p-menubar>
|
<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){
|
@if(user){
|
||||||
<div>Welcome, {{ user.firstName }}</div>
|
<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>
|
||||||
}
|
}
|
||||||
<ng-template #empty> </ng-template>
|
</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>
|
</div>
|
||||||
</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>
|
||||||
|
</nav> -->
|
||||||
|
|
||||||
<nav class="bg-white border-gray-200 dark:bg-gray-900">
|
<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">
|
<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" />
|
<img src="assets/images/header-logo.png" class="h-8" alt="Flowbite Logo" />
|
||||||
</a>
|
</a>
|
||||||
<div class="flex items-center md:order-2 space-x-3 md:space-x-0 rtl:space-x-reverse">
|
<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
|
<button
|
||||||
type="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"
|
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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</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 { CommonModule } from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
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 { faUserGear } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { Collapse, Dropdown, initFlowbite } from 'flowbite';
|
import { Collapse, Dropdown, initFlowbite } from 'flowbite';
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
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 { User } from '../../../../../bizmatch-server/src/models/db.model';
|
||||||
import { emailToDirName, KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
|
import { emailToDirName, KeycloakUser } from '../../../../../bizmatch-server/src/models/main.model';
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
import { SharedService } from '../../services/shared.service';
|
import { SharedService } from '../../services/shared.service';
|
||||||
import { UserService } from '../../services/user.service';
|
import { UserService } from '../../services/user.service';
|
||||||
import { map2User } from '../../utils/utils';
|
import { map2User } from '../../utils/utils';
|
||||||
|
import { DropdownComponent } from '../dropdown/dropdown.component';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'header',
|
selector: 'header',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [CommonModule, RouterModule],
|
imports: [CommonModule, RouterModule, DropdownComponent, FormsModule],
|
||||||
templateUrl: './header.component.html',
|
templateUrl: './header.component.html',
|
||||||
styleUrl: './header.component.scss',
|
styleUrl: './header.component.scss',
|
||||||
})
|
})
|
||||||
|
|
@ -27,15 +30,38 @@ export class HeaderComponent {
|
||||||
faUserGear = faUserGear;
|
faUserGear = faUserGear;
|
||||||
profileUrl: string;
|
profileUrl: string;
|
||||||
env = environment;
|
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() {
|
async ngOnInit() {
|
||||||
const token = await this.keycloakService.getToken();
|
const token = await this.keycloakService.getToken();
|
||||||
this.keycloakUser = map2User(token);
|
this.keycloakUser = map2User(token);
|
||||||
this.user = await this.userService.getByMail(this.keycloakUser.email);
|
if (this.keycloakUser) {
|
||||||
|
this.user = await this.userService.getByMail(this.keycloakUser?.email);
|
||||||
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||||
setTimeout(() => {
|
}
|
||||||
|
|
||||||
|
// setTimeout(() => {
|
||||||
|
// initFlowbite();
|
||||||
|
// });
|
||||||
|
this.router.events.subscribe(event => {
|
||||||
|
if (event instanceof NavigationEnd) {
|
||||||
initFlowbite();
|
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 => {
|
this.sharedService.currentProfilePhoto.subscribe(photoUrl => {
|
||||||
if (photoUrl) {
|
if (photoUrl) {
|
||||||
|
|
@ -43,7 +69,11 @@ export class HeaderComponent {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
toggleFilterDropdown() {
|
||||||
|
if (this.filterDropdown) {
|
||||||
|
this.filterDropdown.toggle();
|
||||||
|
}
|
||||||
|
}
|
||||||
ngAfterViewInit() {}
|
ngAfterViewInit() {}
|
||||||
|
|
||||||
navigateWithState(dest: string, state: any) {
|
navigateWithState(dest: string, state: any) {
|
||||||
|
|
@ -82,4 +112,9 @@ export class HeaderComponent {
|
||||||
this.closeDropdown();
|
this.closeDropdown();
|
||||||
this.closeMobileMenu();
|
this.closeMobileMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.destroy$.next();
|
||||||
|
this.destroy$.complete();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -77,7 +77,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center border border-gray-300 rounded-full p-2">
|
<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()">
|
<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">
|
<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>
|
<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 { Component } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||||
import { initFlowbite } from 'flowbite';
|
|
||||||
import { KeycloakService } from 'keycloak-angular';
|
import { KeycloakService } from 'keycloak-angular';
|
||||||
import onChange from 'on-change';
|
import onChange from 'on-change';
|
||||||
import { KeycloakUser, ListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
import { KeycloakUser, ListingCriteria } from '../../../../../bizmatch-server/src/models/main.model';
|
||||||
|
|
@ -25,7 +24,7 @@ export class HomeComponent {
|
||||||
states = [];
|
states = [];
|
||||||
isMenuOpen = false;
|
isMenuOpen = false;
|
||||||
user: KeycloakUser;
|
user: KeycloakUser;
|
||||||
|
prompt: string;
|
||||||
public constructor(private router: Router, private activatedRoute: ActivatedRoute, public selectOptions: SelectOptionsService, public keycloakService: KeycloakService, private listingsService: ListingsService) {
|
public constructor(private router: Router, private activatedRoute: ActivatedRoute, public selectOptions: SelectOptionsService, public keycloakService: KeycloakService, private listingsService: ListingsService) {
|
||||||
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
|
this.criteria = onChange(getCriteriaStateObject(), getSessionStorageHandler);
|
||||||
resetCriteria(this.criteria);
|
resetCriteria(this.criteria);
|
||||||
|
|
@ -33,9 +32,11 @@ export class HomeComponent {
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
const token = await this.keycloakService.getToken();
|
const token = await this.keycloakService.getToken();
|
||||||
this.user = map2User(token);
|
this.user = map2User(token);
|
||||||
setTimeout(() => {
|
// this.router.events.subscribe(event => {
|
||||||
initFlowbite();
|
// if (event instanceof NavigationEnd) {
|
||||||
});
|
// initFlowbite();
|
||||||
|
// }
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
async changeTab(tabname: 'business' | 'commercialProperty' | 'broker') {
|
async changeTab(tabname: 'business' | 'commercialProperty' | 'broker') {
|
||||||
this.activeTabAction = tabname;
|
this.activeTabAction = tabname;
|
||||||
|
|
@ -48,6 +49,7 @@ export class HomeComponent {
|
||||||
}
|
}
|
||||||
search() {
|
search() {
|
||||||
const data = { keep: true };
|
const data = { keep: true };
|
||||||
|
this.criteria.prompt = this.prompt;
|
||||||
this.router.navigate([`${this.activeTabAction}Listings`]);
|
this.router.navigate([`${this.activeTabAction}Listings`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,11 +85,12 @@
|
||||||
</div> -->
|
</div> -->
|
||||||
<!-- business-listing.component.html -->
|
<!-- business-listing.component.html -->
|
||||||
<!-- <div class="w-full bg-slate-100"> -->
|
<!-- <div class="w-full bg-slate-100"> -->
|
||||||
|
|
||||||
<div class="container mx-auto p-4">
|
<div class="container mx-auto p-4">
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
@for (listing of listings; track listing.id) {
|
@for (listing of listings; track listing.id) {
|
||||||
<div class="bg-white rounded-lg shadow-md overflow-hidden">
|
<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">
|
<div class="flex items-center mb-2">
|
||||||
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="mr-2"></i>
|
<i [class]="selectOptions.getIconAndTextColorType(listing.type)" class="mr-2"></i>
|
||||||
<span [class]="selectOptions.getTextColorType(listing.type)" class="font-semibold">{{ selectOptions.getBusiness(listing.type) }}</span>
|
<span [class]="selectOptions.getTextColorType(listing.type)" class="font-semibold">{{ selectOptions.getBusiness(listing.type) }}</span>
|
||||||
|
|
@ -111,4 +112,17 @@
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</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> -->
|
<!-- </div> -->
|
||||||
|
|
|
||||||
|
|
@ -70,9 +70,10 @@ export class BusinessListingsComponent {
|
||||||
this.search();
|
this.search();
|
||||||
}
|
}
|
||||||
async search() {
|
async search() {
|
||||||
const listingReponse = await this.listingsService.getListings(this.criteria, 'business');
|
this.listings = await this.listingsService.getListingsByPrompt(this.criteria);
|
||||||
this.listings = listingReponse.data;
|
// const listingReponse = await this.listingsService.getListings(this.criteria, 'business');
|
||||||
this.totalRecords = listingReponse.total;
|
// this.listings = listingReponse.data;
|
||||||
|
// this.totalRecords = listingReponse.total;
|
||||||
// this.cdRef.markForCheck();
|
// this.cdRef.markForCheck();
|
||||||
// this.cdRef.detectChanges();
|
// this.cdRef.detectChanges();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,11 @@ export class ListingsService {
|
||||||
constructor(private http: HttpClient) {}
|
constructor(private http: HttpClient) {}
|
||||||
|
|
||||||
async getListings(criteria: ListingCriteria, listingsCategory: 'business' | 'professionals_brokers' | 'commercialProperty'): Promise<ResponseBusinessListingArray | ResponseCommercialPropertyListingArray> {
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
getListingById(id: string, listingsCategory?: 'business' | 'commercialProperty'): Observable<ListingType> {
|
getListingById(id: string, listingsCategory?: 'business' | 'commercialProperty'): Observable<ListingType> {
|
||||||
|
|
|
||||||
|
|
@ -33,13 +33,4 @@ export class UserService {
|
||||||
async getAllStates(): Promise<any> {
|
async getAllStates(): Promise<any> {
|
||||||
return await lastValueFrom(this.http.get<StatesResult[]>(`${this.apiBaseUrl}/bizmatch/user/states/all`));
|
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: '',
|
title: '',
|
||||||
category: 'broker',
|
category: 'broker',
|
||||||
name: '',
|
name: '',
|
||||||
|
prompt: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
export function createLogger(name: string, level: number = INFO, options: any = {}) {
|
export function createLogger(name: string, level: number = INFO, options: any = {}) {
|
||||||
|
|
@ -179,3 +180,58 @@ export function getDialogWidth(dimensions): string {
|
||||||
}
|
}
|
||||||
return dialogWidth;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue