diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c9954dc..f9bb23a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -13,7 +13,20 @@ "Bash(sudo chown:*)", "Bash(chmod:*)", "Bash(npm audit:*)", - "Bash(npm view:*)" + "Bash(npm view:*)", + "Bash(npm run build:ssr:*)", + "Bash(pkill:*)", + "WebSearch", + "Bash(lsof:*)", + "Bash(xargs:*)", + "Bash(curl:*)", + "Bash(grep:*)", + "Bash(cat:*)", + "Bash(NODE_ENV=development npm run build:ssr:*)", + "Bash(ls:*)", + "WebFetch(domain:angular.dev)", + "Bash(killall:*)", + "Bash(echo:*)" ] } } diff --git a/DOCKER_SYNC_GUIDE.md b/DOCKER_SYNC_GUIDE.md new file mode 100644 index 0000000..e688d5b --- /dev/null +++ b/DOCKER_SYNC_GUIDE.md @@ -0,0 +1,27 @@ +# Docker Code Sync Guide + +If you have made changes to the backend code and they don't seem to take effect (even though the files on disk are updated), it's because the Docker container is running from a pre-compiled `dist/` directory. + +### The Problem +The `bizmatch-app` container compiles the TypeScript code *only once* when the container starts. It does not automatically watch for changes and recompile while running. + +### The Solution +You must restart or recreate the container to trigger a new build. + +**Option 1: Quick Restart (Recommended)** +Run this in the `bizmatch-server` directory: +```bash +docker-compose restart app +``` + +**Option 2: Force Rebuild (If changes aren't picked up)** +If a simple restart doesn't work, use this to force a fresh build: +```bash +docker-compose up -d --build app +``` + +### Summary for Other Laptops +1. **Pull** the latest changes from Git. +2. **Execute** `docker-compose restart app`. +3. **Verify** the logs for the new `WARN` debug messages. +. \ No newline at end of file diff --git a/bizmatch-server/prod.dump b/bizmatch-server/prod.dump old mode 100755 new mode 100644 diff --git a/bizmatch-server/src/listings/business-listing.service.ts b/bizmatch-server/src/listings/business-listing.service.ts index ac7be83..5f5a62c 100644 --- a/bizmatch-server/src/listings/business-listing.service.ts +++ b/bizmatch-server/src/listings/business-listing.service.ts @@ -5,7 +5,7 @@ import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import { Logger } from 'winston'; import { ZodError } from 'zod'; import * as schema from '../drizzle/schema'; -import { businesses_json, PG_CONNECTION, users_json } from '../drizzle/schema'; +import { businesses_json, PG_CONNECTION } from '../drizzle/schema'; import { GeoService } from '../geo/geo.service'; import { BusinessListing, BusinessListingSchema } from '../models/db.model'; import { BusinessListingCriteria, JwtUser } from '../models/main.model'; @@ -18,27 +18,30 @@ export class BusinessListingService { @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger, @Inject(PG_CONNECTION) private conn: NodePgDatabase, private geoService?: GeoService, - ) {} + ) { } private getWhereConditions(criteria: BusinessListingCriteria, user: JwtUser): SQL[] { const whereConditions: SQL[] = []; + this.logger.info('getWhereConditions start', { criteria: JSON.stringify(criteria) }); if (criteria.city && criteria.searchType === 'exact') { whereConditions.push(sql`(${businesses_json.data}->'location'->>'name') ILIKE ${criteria.city.name}`); } if (criteria.city && criteria.radius && criteria.searchType === 'radius' && criteria.radius) { + this.logger.debug('Adding radius search filter', { city: criteria.city.name, radius: criteria.radius }); const cityGeo = this.geoService.getCityWithCoords(criteria.state, criteria.city.name); - whereConditions.push(sql`${getDistanceQuery(businesses_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`); + whereConditions.push(sql`(${getDistanceQuery(businesses_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius})`); } - if (criteria.types && Array.isArray(criteria.types) && criteria.types.length > 0) { - const validTypes = criteria.types.filter(t => t !== null && t !== undefined && t !== ''); - if (validTypes.length > 0) { - whereConditions.push(inArray(sql`${businesses_json.data}->>'type'`, validTypes)); - } + if (criteria.types && criteria.types.length > 0) { + this.logger.warn('Adding business category filter', { types: criteria.types }); + // Use explicit SQL with IN for robust JSONB comparison + const typeValues = criteria.types.map(t => sql`${t}`); + whereConditions.push(sql`((${businesses_json.data}->>'type') IN (${sql.join(typeValues, sql`, `)}))`); } if (criteria.state) { + this.logger.debug('Adding state filter', { state: criteria.state }); whereConditions.push(sql`(${businesses_json.data}->'location'->>'state') = ${criteria.state}`); } @@ -105,27 +108,30 @@ export class BusinessListingService { if (criteria.title && criteria.title.trim() !== '') { const searchTerm = `%${criteria.title.trim()}%`; whereConditions.push( - or( - sql`(${businesses_json.data}->>'title') ILIKE ${searchTerm}`, - sql`(${businesses_json.data}->>'description') ILIKE ${searchTerm}` - ) + sql`((${businesses_json.data}->>'title') ILIKE ${searchTerm} OR (${businesses_json.data}->>'description') ILIKE ${searchTerm})` ); } if (criteria.brokerName) { const { firstname, lastname } = splitName(criteria.brokerName); if (firstname === lastname) { - whereConditions.push(sql`(${users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} OR (${users_json.data}->>'lastname') ILIKE ${`%${lastname}%`}`); + whereConditions.push( + sql`((${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} OR (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`})` + ); } else { - whereConditions.push(sql`(${users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} AND (${users_json.data}->>'lastname') ILIKE ${`%${lastname}%`}`); + whereConditions.push( + sql`((${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} AND (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`})` + ); } } if (criteria.email) { - whereConditions.push(eq(users_json.email, criteria.email)); + whereConditions.push(eq(schema.users_json.email, criteria.email)); } if (user?.role !== 'admin') { - whereConditions.push(or(eq(businesses_json.email, user?.email), sql`(${businesses_json.data}->>'draft')::boolean IS NOT TRUE`)); + whereConditions.push( + sql`((${businesses_json.email} = ${user?.email || null}) OR (${businesses_json.data}->>'draft')::boolean IS NOT TRUE)` + ); } - whereConditions.push(and(sql`(${users_json.data}->>'customerType') = 'professional'`, sql`(${users_json.data}->>'customerSubType') = 'broker'`)); + this.logger.warn('whereConditions count', { count: whereConditions.length }); return whereConditions; } @@ -135,24 +141,21 @@ export class BusinessListingService { const query = this.conn .select({ business: businesses_json, - brokerFirstName: sql`${users_json.data}->>'firstname'`.as('brokerFirstName'), - brokerLastName: sql`${users_json.data}->>'lastname'`.as('brokerLastName'), + brokerFirstName: sql`${schema.users_json.data}->>'firstname'`.as('brokerFirstName'), + brokerLastName: sql`${schema.users_json.data}->>'lastname'`.as('brokerLastName'), }) .from(businesses_json) - .leftJoin(users_json, eq(businesses_json.email, users_json.email)); + .leftJoin(schema.users_json, eq(businesses_json.email, schema.users_json.email)); const whereConditions = this.getWhereConditions(criteria, user); - // Uncomment for debugging filter issues: - // this.logger.info('Filter Criteria:', { criteria }); - // this.logger.info('Where Conditions Count:', { count: whereConditions.length }); + this.logger.warn('Filter Criteria:', { criteria: JSON.stringify(criteria) }); if (whereConditions.length > 0) { - const whereClause = and(...whereConditions); - query.where(whereClause); + const whereClause = sql.join(whereConditions, sql` AND `); + query.where(sql`(${whereClause})`); - // Uncomment for debugging SQL queries: - // this.logger.info('Generated SQL:', { sql: query.toSQL() }); + this.logger.warn('Generated SQL:', { sql: query.toSQL().sql, params: query.toSQL().params }); } // Sortierung @@ -228,13 +231,13 @@ export class BusinessListingService { } async getBusinessListingsCount(criteria: BusinessListingCriteria, user: JwtUser): Promise { - const countQuery = this.conn.select({ value: count() }).from(businesses_json).leftJoin(users_json, eq(businesses_json.email, users_json.email)); + const countQuery = this.conn.select({ value: count() }).from(businesses_json).leftJoin(schema.users_json, eq(businesses_json.email, schema.users_json.email)); const whereConditions = this.getWhereConditions(criteria, user); if (whereConditions.length > 0) { - const whereClause = and(...whereConditions); - countQuery.where(whereClause); + const whereClause = sql.join(whereConditions, sql` AND `); + countQuery.where(sql`(${whereClause})`); } const [{ value: totalCount }] = await countQuery; diff --git a/bizmatch-server/src/listings/commercial-property.service.ts b/bizmatch-server/src/listings/commercial-property.service.ts index 88e05f7..88f5641 100644 --- a/bizmatch-server/src/listings/commercial-property.service.ts +++ b/bizmatch-server/src/listings/commercial-property.service.ts @@ -10,7 +10,7 @@ import { FileService } from '../file/file.service'; import { GeoService } from '../geo/geo.service'; import { CommercialPropertyListing, CommercialPropertyListingSchema } from '../models/db.model'; import { CommercialPropertyListingCriteria, JwtUser } from '../models/main.model'; -import { getDistanceQuery } from '../utils'; +import { getDistanceQuery, splitName } from '../utils'; import { generateSlug, extractShortIdFromSlug, isSlug } from '../utils/slug.utils'; @Injectable() @@ -20,7 +20,7 @@ export class CommercialPropertyService { @Inject(PG_CONNECTION) private conn: NodePgDatabase, private fileService?: FileService, private geoService?: GeoService, - ) {} + ) { } private getWhereConditions(criteria: CommercialPropertyListingCriteria, user: JwtUser): SQL[] { const whereConditions: SQL[] = []; @@ -32,7 +32,10 @@ export class CommercialPropertyService { whereConditions.push(sql`${getDistanceQuery(commercials_json, cityGeo.latitude, cityGeo.longitude)} <= ${criteria.radius}`); } if (criteria.types && criteria.types.length > 0) { - whereConditions.push(inArray(sql`${commercials_json.data}->>'type'`, criteria.types)); + this.logger.warn('Adding commercial property type filter', { types: criteria.types }); + // Use explicit SQL with IN for robust JSONB comparison + const typeValues = criteria.types.map(t => sql`${t}`); + whereConditions.push(sql`((${commercials_json.data}->>'type') IN (${sql.join(typeValues, sql`, `)}))`); } if (criteria.state) { @@ -48,12 +51,32 @@ export class CommercialPropertyService { } if (criteria.title) { - whereConditions.push(sql`(${commercials_json.data}->>'title') ILIKE ${`%${criteria.title}%`} OR (${commercials_json.data}->>'description') ILIKE ${`%${criteria.title}%`}`); + whereConditions.push( + sql`((${commercials_json.data}->>'title') ILIKE ${`%${criteria.title}%`} OR (${commercials_json.data}->>'description') ILIKE ${`%${criteria.title}%`})` + ); } + + if (criteria.brokerName) { + const { firstname, lastname } = splitName(criteria.brokerName); + if (firstname === lastname) { + // Single word: search either first OR last name + whereConditions.push( + sql`((${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} OR (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`})` + ); + } else { + // Multiple words: search both first AND last name + whereConditions.push( + sql`((${schema.users_json.data}->>'firstname') ILIKE ${`%${firstname}%`} AND (${schema.users_json.data}->>'lastname') ILIKE ${`%${lastname}%`})` + ); + } + } + if (user?.role !== 'admin') { - whereConditions.push(or(eq(commercials_json.email, user?.email), sql`(${commercials_json.data}->>'draft')::boolean IS NOT TRUE`)); + whereConditions.push( + sql`((${commercials_json.email} = ${user?.email || null}) OR (${commercials_json.data}->>'draft')::boolean IS NOT TRUE)` + ); } - // whereConditions.push(and(eq(schema.users.customerType, 'professional'))); + this.logger.warn('whereConditions count', { count: whereConditions.length }); return whereConditions; } // #### Find by criteria ######################################## @@ -63,9 +86,13 @@ export class CommercialPropertyService { const query = this.conn.select({ commercial: commercials_json }).from(commercials_json).leftJoin(schema.users_json, eq(commercials_json.email, schema.users_json.email)); const whereConditions = this.getWhereConditions(criteria, user); + this.logger.warn('Filter Criteria:', { criteria: JSON.stringify(criteria) }); + if (whereConditions.length > 0) { - const whereClause = and(...whereConditions); - query.where(whereClause); + const whereClause = sql.join(whereConditions, sql` AND `); + query.where(sql`(${whereClause})`); + + this.logger.warn('Generated SQL:', { sql: query.toSQL().sql, params: query.toSQL().params }); } // Sortierung switch (criteria.sortBy) { @@ -103,8 +130,8 @@ export class CommercialPropertyService { const whereConditions = this.getWhereConditions(criteria, user); if (whereConditions.length > 0) { - const whereClause = and(...whereConditions); - countQuery.where(whereClause); + const whereClause = sql.join(whereConditions, sql` AND `); + countQuery.where(sql`(${whereClause})`); } const [{ value: totalCount }] = await countQuery; diff --git a/bizmatch-server/src/models/db.model.ts b/bizmatch-server/src/models/db.model.ts index 8196ba0..c1ee0b5 100644 --- a/bizmatch-server/src/models/db.model.ts +++ b/bizmatch-server/src/models/db.model.ts @@ -34,6 +34,7 @@ export const CustomerTypeEnum = z.enum(['buyer', 'seller', 'professional']); export const SubscriptionTypeEnum = z.enum(['free', 'professional', 'broker']); export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']); export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']); +export const ShareCategoryEnum = z.enum(['commercialProperty', 'business', 'user']); export const ZodEventTypeEnum = z.enum(['view', 'print', 'email', 'facebook', 'x', 'linkedin', 'contact', 'favorite', 'emailus', 'pricing']); export type EventTypeEnum = z.infer; const PropertyTypeEnum = z.enum(['retail', 'land', 'industrial', 'office', 'mixedUse', 'multifamily', 'uncategorized']); @@ -186,6 +187,7 @@ export const UserSchema = z updated: z.date().optional().nullable(), subscriptionId: z.string().optional().nullable(), subscriptionPlan: SubscriptionTypeEnum.optional().nullable(), + favoritesForUser: z.array(z.string()), showInDirectory: z.boolean(), }) .superRefine((data, ctx) => { @@ -369,7 +371,7 @@ export const ShareByEMailSchema = z.object({ listingTitle: z.string().optional().nullable(), url: z.string().url({ message: 'Invalid URL format' }).optional().nullable(), id: z.string().optional().nullable(), - type: ListingsCategoryEnum, + type: ShareCategoryEnum, }); export type ShareByEMail = z.infer; diff --git a/bizmatch-server/src/models/main.model.ts b/bizmatch-server/src/models/main.model.ts index aa5459a..c4a5a88 100644 --- a/bizmatch-server/src/models/main.model.ts +++ b/bizmatch-server/src/models/main.model.ts @@ -96,6 +96,7 @@ export interface CommercialPropertyListingCriteria extends ListCriteria { minPrice: number; maxPrice: number; title: string; + brokerName: string; criteriaType: 'commercialPropertyListings'; } export interface UserListingCriteria extends ListCriteria { @@ -358,6 +359,7 @@ export function createDefaultUser(email: string, firstname: string, lastname: st updated: new Date(), subscriptionId: null, subscriptionPlan: subscriptionPlan, + favoritesForUser: [], showInDirectory: false, }; } diff --git a/bizmatch/src/app/components/search-modal/search-modal-commercial.component.html b/bizmatch/src/app/components/search-modal/search-modal-commercial.component.html index a506913..994e3e8 100644 --- a/bizmatch/src/app/components/search-modal/search-modal-commercial.component.html +++ b/bizmatch/src/app/components/search-modal/search-modal-commercial.component.html @@ -39,6 +39,9 @@ Title: {{ criteria.title }} + + Broker: {{ criteria.brokerName }} + @if(criteria.criteriaType==='commercialPropertyListings') {
@@ -113,6 +116,17 @@ placeholder="Select categories" >
+
+ + +
} @@ -146,6 +160,9 @@ Title: {{ criteria.title }} + + Broker: {{ criteria.brokerName }} + @if(criteria.criteriaType==='commercialPropertyListings') {
@@ -217,6 +234,17 @@ placeholder="e.g. Office Space" />
+
+ + +
} diff --git a/bizmatch/src/app/components/search-modal/search-modal-commercial.component.ts b/bizmatch/src/app/components/search-modal/search-modal-commercial.component.ts index 1ee6049..a6d5d7d 100644 --- a/bizmatch/src/app/components/search-modal/search-modal-commercial.component.ts +++ b/bizmatch/src/app/components/search-modal/search-modal-commercial.component.ts @@ -48,7 +48,7 @@ export class SearchModalCommercialComponent implements OnInit, OnDestroy { private filterStateService: FilterStateService, private listingService: ListingsService, private searchService: SearchService, - ) {} + ) { } ngOnInit(): void { // Load counties @@ -143,6 +143,9 @@ export class SearchModalCommercialComponent implements OnInit, OnDestroy { case 'title': updates.title = null; break; + case 'brokerName': + updates.brokerName = null; + break; } this.updateCriteria(updates); @@ -280,6 +283,7 @@ export class SearchModalCommercialComponent implements OnInit, OnDestroy { minPrice: null, maxPrice: null, title: null, + brokerName: null, prompt: null, page: 1, start: 0, @@ -290,7 +294,15 @@ export class SearchModalCommercialComponent implements OnInit, OnDestroy { hasActiveFilters(): boolean { if (!this.criteria) return false; - return !!(this.criteria.state || this.criteria.city || this.criteria.minPrice || this.criteria.maxPrice || this.criteria.types?.length || this.criteria.title); + return !!( + this.criteria.state || + this.criteria.city || + this.criteria.minPrice || + this.criteria.maxPrice || + this.criteria.types?.length || + this.criteria.title || + this.criteria.brokerName + ); } trackByFn(item: GeoResult): any { diff --git a/bizmatch/src/app/pages/details/details-user/details-user.component.html b/bizmatch/src/app/pages/details/details-user/details-user.component.html index fa0958c..b7ffe4f 100644 --- a/bizmatch/src/app/pages/details/details-user/details-user.component.html +++ b/bizmatch/src/app/pages/details/details-user/details-user.component.html @@ -47,6 +47,68 @@

{{ user.description }}

+ +
+ @if(user && keycloakUser && (user?.email===keycloakUser?.email || (authService.isAdmin() | async))){ +
+ +
+ } + @if(keycloakUser && keycloakUser.email !== user.email){ +
+ +
+ } + + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+
+

Company Profile

@@ -142,8 +204,6 @@
} - } @if( user?.email===keycloakUser?.email || (authService.isAdmin() | async)){ - } diff --git a/bizmatch/src/app/pages/details/details-user/details-user.component.ts b/bizmatch/src/app/pages/details/details-user/details-user.component.ts index bb3d5a4..b57d25a 100644 --- a/bizmatch/src/app/pages/details/details-user/details-user.component.ts +++ b/bizmatch/src/app/pages/details/details-user/details-user.component.ts @@ -3,11 +3,14 @@ import { NgOptimizedImage } from '@angular/common'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs'; -import { BusinessListing, CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; +import { BusinessListing, CommercialPropertyListing, User, ShareByEMail, EventTypeEnum } from '../../../../../../bizmatch-server/src/models/db.model'; import { KeycloakUser, emailToDirName } from '../../../../../../bizmatch-server/src/models/main.model'; import { environment } from '../../../../environments/environment'; import { BreadcrumbItem, BreadcrumbsComponent } from '../../../components/breadcrumbs/breadcrumbs.component'; import { AuthService } from '../../../services/auth.service'; +import { AuditService } from '../../../services/audit.service'; +import { EMailService } from '../../../components/email/email.service'; +import { MessageService } from '../../../components/message/message.service'; import { HistoryService } from '../../../services/history.service'; import { ImageService } from '../../../services/image.service'; import { ListingsService } from '../../../services/listings.service'; @@ -15,11 +18,12 @@ import { SelectOptionsService } from '../../../services/select-options.service'; import { UserService } from '../../../services/user.service'; import { SharedModule } from '../../../shared/shared/shared.module'; import { formatPhoneNumber, map2User } from '../../../utils/utils'; +import { ShareButton } from 'ngx-sharebuttons/button'; @Component({ selector: 'app-details-user', standalone: true, - imports: [SharedModule, BreadcrumbsComponent, NgOptimizedImage], + imports: [SharedModule, BreadcrumbsComponent, NgOptimizedImage, ShareButton], templateUrl: './details-user.component.html', styleUrl: '../details.scss', }) @@ -47,12 +51,14 @@ export class DetailsUserComponent { private router: Router, private userService: UserService, private listingsService: ListingsService, - public selectOptions: SelectOptionsService, private sanitizer: DomSanitizer, private imageService: ImageService, public historyService: HistoryService, public authService: AuthService, + private auditService: AuditService, + private emailService: EMailService, + private messageService: MessageService, ) {} async ngOnInit() { @@ -66,4 +72,82 @@ export class DetailsUserComponent { this.companyOverview = this.sanitizer.bypassSecurityTrustHtml(this.user.companyOverview ? this.user.companyOverview : ''); this.offeredServices = this.sanitizer.bypassSecurityTrustHtml(this.user.offeredServices ? this.user.offeredServices : ''); } + + /** + * Add professional to favorites + */ + async save() { + await this.listingsService.addToFavorites(this.user.id, 'user'); + if (!this.user.favoritesForUser) { + this.user.favoritesForUser = []; + } + this.user.favoritesForUser.push(this.keycloakUser.email); + this.auditService.createEvent(this.user.id, 'favorite', this.keycloakUser?.email); + } + + /** + * Check if already in favorites + */ + isAlreadyFavorite(): boolean { + if (!this.keycloakUser?.email || !this.user?.favoritesForUser) return false; + return this.user.favoritesForUser.includes(this.keycloakUser.email); + } + + /** + * Show email sharing modal + */ + async showShareByEMail() { + const result = await this.emailService.showShareByEMail({ + yourEmail: this.keycloakUser ? this.keycloakUser.email : '', + yourName: this.keycloakUser ? `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` : '', + recipientEmail: '', + url: environment.mailinfoUrl, + listingTitle: `${this.user.firstname} ${this.user.lastname} - ${this.user.companyName}`, + id: this.user.id, + type: 'user', + }); + if (result) { + this.auditService.createEvent(this.user.id, 'email', this.keycloakUser?.email, result); + this.messageService.addMessage({ + severity: 'success', + text: 'Your Email has been sent', + duration: 5000, + }); + } + } + + /** + * Create audit event + */ + createEvent(eventType: EventTypeEnum) { + this.auditService.createEvent(this.user.id, eventType, this.keycloakUser?.email); + } + + /** + * Share to Facebook + */ + shareToFacebook() { + const url = encodeURIComponent(window.location.href); + window.open(`https://www.facebook.com/sharer/sharer.php?u=${url}`, '_blank', 'width=600,height=400'); + this.createEvent('facebook'); + } + + /** + * Share to Twitter/X + */ + shareToTwitter() { + const url = encodeURIComponent(window.location.href); + const text = encodeURIComponent(`Check out ${this.user.firstname} ${this.user.lastname} - ${this.user.companyName}`); + window.open(`https://twitter.com/intent/tweet?url=${url}&text=${text}`, '_blank', 'width=600,height=400'); + this.createEvent('x'); + } + + /** + * Share to LinkedIn + */ + shareToLinkedIn() { + const url = encodeURIComponent(window.location.href); + window.open(`https://www.linkedin.com/sharing/share-offsite/?url=${url}`, '_blank', 'width=600,height=400'); + this.createEvent('linkedin'); + } } diff --git a/bizmatch/src/app/pages/listings/broker-listings/broker-listings.component.html b/bizmatch/src/app/pages/listings/broker-listings/broker-listings.component.html index 3e5d8be..e8dfae6 100644 --- a/bizmatch/src/app/pages/listings/broker-listings/broker-listings.component.html +++ b/bizmatch/src/app/pages/listings/broker-listings/broker-listings.component.html @@ -34,7 +34,22 @@ @for (user of users; track user) {
+ class="bg-white rounded-lg drop-shadow-custom-bg-mobile md:drop-shadow-custom-bg p-6 flex flex-col justify-between hover:shadow-2xl transition-all duration-300 hover:scale-[1.02] group relative"> + +
+ @if(currentUser) { + + } + +
@if(user.hasProfile){ { + // Get current logged-in user + const token = await this.authService.getToken(); + this.currentUser = map2User(token); + // Subscribe to FilterStateService for criteria changes this.filterStateService .getState$('brokerListings') @@ -144,4 +151,86 @@ export class BrokerListingsComponent implements OnInit, OnDestroy { this.criteria = assignProperties(this.criteria, modalResult.criteria); } } + + /** + * Check if professional/user is already in current user's favorites + */ + isFavorite(professional: User): boolean { + if (!this.currentUser?.email || !professional.favoritesForUser) return false; + return professional.favoritesForUser.includes(this.currentUser.email); + } + + /** + * Toggle favorite status for a professional + */ + async toggleFavorite(event: Event, professional: User): Promise { + event.stopPropagation(); + event.preventDefault(); + + if (!this.currentUser?.email) { + // User not logged in - redirect to login + this.router.navigate(['/login']); + return; + } + + try { + if (this.isFavorite(professional)) { + // Remove from favorites + await this.listingsService.removeFavorite(professional.id, 'user'); + professional.favoritesForUser = professional.favoritesForUser.filter( + email => email !== this.currentUser!.email + ); + } else { + // Add to favorites + await this.listingsService.addToFavorites(professional.id, 'user'); + if (!professional.favoritesForUser) { + professional.favoritesForUser = []; + } + professional.favoritesForUser.push(this.currentUser.email); + } + this.cdRef.detectChanges(); + } catch (error) { + console.error('Error toggling favorite:', error); + } + } + + /** + * Share professional profile + */ + async shareProfessional(event: Event, user: User): Promise { + event.stopPropagation(); + event.preventDefault(); + + const url = `${window.location.origin}/details-user/${user.id}`; + const title = `${user.firstname} ${user.lastname} - ${user.companyName}`; + + // Try native share API first (works on mobile and some desktop browsers) + if (navigator.share) { + try { + await navigator.share({ + title: title, + text: `Check out this professional: ${title}`, + url: url, + }); + } catch (err) { + // User cancelled or share failed - fall back to clipboard + this.copyToClipboard(url); + } + } else { + // Fallback: open Facebook share dialog + const encodedUrl = encodeURIComponent(url); + window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`, '_blank', 'width=600,height=400'); + } + } + + /** + * Copy URL to clipboard and show feedback + */ + private copyToClipboard(url: string): void { + navigator.clipboard.writeText(url).then(() => { + console.log('Link copied to clipboard!'); + }).catch(err => { + console.error('Failed to copy link:', err); + }); + } } diff --git a/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.html b/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.html index d36099e..37689ef 100644 --- a/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.html +++ b/bizmatch/src/app/pages/listings/commercial-property-listings/commercial-property-listings.component.html @@ -23,17 +23,23 @@
@for (listing of listings; track listing.id) {
- - @if(user) { - - } + +
+ @if(user) { + + } + +
@if (listing.imageOrder?.length>0){ { + event.stopPropagation(); + event.preventDefault(); + + const url = `${window.location.origin}/commercial-property/${listing.slug || listing.id}`; + const title = listing.title || 'Commercial Property Listing'; + + // Try native share API first (works on mobile and some desktop browsers) + if (navigator.share) { + try { + await navigator.share({ + title: title, + text: `Check out this property: ${title}`, + url: url, + }); + } catch (err) { + // User cancelled or share failed - fall back to clipboard + this.copyToClipboard(url); + } + } else { + // Fallback: open Facebook share dialog + const encodedUrl = encodeURIComponent(url); + window.open(`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`, '_blank', 'width=600,height=400'); + } + } + + /** + * Copy URL to clipboard and show feedback + */ + private copyToClipboard(url: string): void { + navigator.clipboard.writeText(url).then(() => { + console.log('Link copied to clipboard!'); + }).catch(err => { + console.error('Failed to copy link:', err); + }); + } } diff --git a/bizmatch/src/app/pages/subscription/account/account.component.html b/bizmatch/src/app/pages/subscription/account/account.component.html index 0d4ae7d..fe31d4e 100644 --- a/bizmatch/src/app/pages/subscription/account/account.component.html +++ b/bizmatch/src/app/pages/subscription/account/account.component.html @@ -233,7 +233,6 @@ [Add more licenses or remove existing ones.]
- }