Feture #52: Social Media Integration, BugFix: #89 Sates as ng-select, send Listing as EMail to friend

This commit is contained in:
Andreas Knuth 2024-08-30 14:25:29 +02:00
parent 630c31cfc9
commit f4f576d4a9
16 changed files with 342 additions and 164 deletions

View File

@ -1,5 +1,5 @@
import { Body, Controller, Post } from '@nestjs/common'; import { Body, Controller, Post } from '@nestjs/common';
import { User } from 'src/models/db.model'; import { ShareByEMail, User } from 'src/models/db.model';
import { ErrorResponse, MailInfo } from '../models/main.model'; import { ErrorResponse, MailInfo } from '../models/main.model';
import { MailService } from './mail.service.js'; import { MailService } from './mail.service.js';
@ -18,4 +18,8 @@ export class MailController {
sendSubscriptionConfirmation(@Body() user: User): Promise<void | ErrorResponse> { sendSubscriptionConfirmation(@Body() user: User): Promise<void | ErrorResponse> {
return this.mailService.sendSubscriptionConfirmation(user); return this.mailService.sendSubscriptionConfirmation(user);
} }
@Post('send2Friend')
send2Friend(@Body() shareByEMail: ShareByEMail): Promise<void | ErrorResponse> {
return this.mailService.send2Friend(shareByEMail);
}
} }

View File

@ -3,7 +3,7 @@ import { BadRequestException, Injectable } from '@nestjs/common';
import path, { join } from 'path'; import path, { join } from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { ZodError } from 'zod'; import { ZodError } from 'zod';
import { SenderSchema, User } from '../models/db.model.js'; import { SenderSchema, ShareByEMail, ShareByEMailSchema, User } from '../models/db.model.js';
import { ErrorResponse, MailInfo, isEmpty } from '../models/main.model.js'; import { ErrorResponse, MailInfo, isEmpty } from '../models/main.model.js';
import { UserService } from '../user/user.service.js'; import { UserService } from '../user/user.service.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@ -96,4 +96,33 @@ export class MailService {
}, },
}); });
} }
async send2Friend(shareByEMail: ShareByEMail): Promise<void | ErrorResponse> {
try {
const validatedSender = ShareByEMailSchema.parse(shareByEMail);
} catch (error) {
if (error instanceof ZodError) {
const formattedErrors = error.errors.map(err => ({
field: err.path.join('.'),
message: err.message,
}));
throw new BadRequestException(formattedErrors);
}
throw error;
}
await this.mailerService.sendMail({
to: shareByEMail.recipientEmail,
from: `"Bizmatch.net" <info@bizmatch.net>`,
subject: `${shareByEMail.type === 'business' ? 'Business' : 'Commercial Property'} For Sale: ${shareByEMail.listingTitle}`,
//template: './inquiry', // `.hbs` extension is appended automatically
template: join(__dirname, '../..', 'mail/templates/send2Friend.hbs'),
context: {
name: shareByEMail.name,
email: shareByEMail.email,
listingTitle: shareByEMail.listingTitle,
url: shareByEMail.url,
id: shareByEMail.id,
type: shareByEMail.type,
},
});
}
} }

View File

@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Notification</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
color: #333333;
margin: 0;
padding: 0;
}
.email-container {
max-width: 600px;
margin: 20px auto;
background-color: #ffffff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.header {
text-align: center;
border-bottom: 1px solid #dddddd;
padding-bottom: 20px;
}
.header h1 {
font-size: 24px;
color: #333333;
}
.content {
margin-top: 20px;
}
.content p {
font-size: 16px;
line-height: 1.6;
}
.content .plan-info {
font-weight: bold;
color: #0056b3;
}
.footer {
margin-top: 30px;
text-align: center;
font-size: 14px;
color: #888888;
}
</style>
</head>
<body>
<div class="email-container">
<div class="header">
<h1>Notification</h1>
</div>
<div class="content">
<p>Hello,</p>
<p>Your friend {{name}} ({{email}}) believed you might find this <b>{{#if (eq type "commercialProperty")}}Commercial Property{{else if (eq type "business")}}Business{{/if}} for sale listing </b> on <a href="{{url}}">bizmatch.net</a> interesting.</p>
<span class="info-value"><a href="{{url}}/listing/{{id}}">{{listingTitle}}</a></span>
<p>Bizmatch is one of the most reliable platforms for buying and selling businesses.</p>
<p>Best regards,</p>
<p>The Bizmatch Support Team</p>
</div>
<div class="footer">
<p>© 2024 Bizmatch. All rights reserved.</p>
</div>
</div>
</body>
</html>

View File

@ -301,3 +301,13 @@ export const SenderSchema = z.object({
comments: z.string().min(10, { message: 'Comments must be at least 10 characters long' }), comments: z.string().min(10, { message: 'Comments must be at least 10 characters long' }),
}); });
export type Sender = z.infer<typeof SenderSchema>; export type Sender = z.infer<typeof SenderSchema>;
export const ShareByEMailSchema = z.object({
name: z.string().min(6, { message: 'Name must be at least 6 characters long' }),
recipientEmail: z.string().email({ message: 'Invalid email address' }),
email: z.string().email({ message: 'Invalid email address' }),
listingTitle: z.string().optional().nullable(),
url: z.string().url({ message: 'Invalid URL format' }).optional().nullable(),
id: z.string().optional().nullable(),
type: ListingsCategoryEnum,
});
export type ShareByEMail = z.infer<typeof ShareByEMailSchema>;

View File

@ -36,3 +36,4 @@
<app-message-container></app-message-container> <app-message-container></app-message-container>
<app-search-modal></app-search-modal> <app-search-modal></app-search-modal>
<app-confirmation></app-confirmation> <app-confirmation></app-confirmation>
<app-email></app-email>

View File

@ -7,6 +7,7 @@ import { filter } from 'rxjs/operators';
import build from '../build'; import build from '../build';
import { ConfirmationComponent } from './components/confirmation/confirmation.component'; import { ConfirmationComponent } from './components/confirmation/confirmation.component';
import { ConfirmationService } from './components/confirmation/confirmation.service'; import { ConfirmationService } from './components/confirmation/confirmation.service';
import { EMailComponent } from './components/email/email.component';
import { FooterComponent } from './components/footer/footer.component'; import { FooterComponent } from './components/footer/footer.component';
import { HeaderComponent } from './components/header/header.component'; import { HeaderComponent } from './components/header/header.component';
import { MessageContainerComponent } from './components/message/message-container.component'; import { MessageContainerComponent } from './components/message/message-container.component';
@ -17,7 +18,7 @@ import { UserService } from './services/user.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
standalone: true, standalone: true,
imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent], imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent, EMailComponent],
providers: [], providers: [],
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrl: './app.component.scss', styleUrl: './app.component.scss',

View File

@ -0,0 +1,43 @@
<!-- Main modal -->
<div *ngIf="eMailService.modalVisible$ | async" id="authentication-modal" tabindex="-1" class="z-40 fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full flex items-center justify-center">
<div class="relative p-4 w-full max-w-md max-h-full">
<!-- Modal content -->
<div class="relative bg-white rounded-lg shadow dark:bg-gray-700">
<!-- Modal header -->
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600">
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">Email listing to a friend</h3>
<button
(click)="eMailService.reject()"
type="button"
class="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
>
<svg class="w-3 h-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6" />
</svg>
<span class="sr-only">Close modal</span>
</button>
</div>
<!-- Modal body -->
<div class="p-4 md:p-5">
<form class="space-y-4" action="#">
<div>
<app-validated-input label="Your Email" name="email" [(ngModel)]="shareByEMail.email"></app-validated-input>
</div>
<div>
<app-validated-input label="Your Name" name="name" [(ngModel)]="shareByEMail.name"></app-validated-input>
</div>
<div>
<app-validated-input label="Your Friend's EMail" name="recipientEmail" [(ngModel)]="shareByEMail.recipientEmail"></app-validated-input>
</div>
<button
(click)="sendMail()"
class="w-full text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
>
Send EMail
</button>
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,40 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { ShareByEMail } from '../../../../../bizmatch-server/src/models/db.model';
import { MailService } from '../../services/mail.service';
import { ValidatedInputComponent } from '../validated-input/validated-input.component';
import { ValidationMessagesService } from '../validation-messages.service';
import { EMailService } from './email.service';
@UntilDestroy()
@Component({
selector: 'app-email',
standalone: true,
imports: [CommonModule, FormsModule, ValidatedInputComponent],
templateUrl: './email.component.html',
template: ``,
})
export class EMailComponent {
shareByEMail: ShareByEMail = {};
constructor(public eMailService: EMailService, private mailService: MailService, private validationMessagesService: ValidationMessagesService) {}
ngOnInit() {
this.eMailService.shareByEMail$.pipe(untilDestroyed(this)).subscribe(val => {
this.shareByEMail = val;
});
}
async sendMail() {
try {
const result = await this.mailService.mailToFriend(this.shareByEMail);
this.eMailService.accept();
} catch (error) {
if (error.error && Array.isArray(error.error?.message)) {
this.validationMessagesService.updateMessages(error.error.message);
}
}
}
ngOnDestroy() {
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
}
}

View File

@ -0,0 +1,33 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { ShareByEMail } from '../../../../../bizmatch-server/src/models/db.model';
@Injectable({
providedIn: 'root',
})
export class EMailService {
private modalVisibleSubject = new BehaviorSubject<boolean>(false);
private shareByEMailSubject = new BehaviorSubject<ShareByEMail>({});
private resolvePromise!: (value: boolean) => void;
modalVisible$: Observable<boolean> = this.modalVisibleSubject.asObservable();
shareByEMail$: Observable<ShareByEMail> = this.shareByEMailSubject.asObservable();
showShareByEMail(shareByEMail: ShareByEMail): Promise<boolean> {
this.shareByEMailSubject.next(shareByEMail);
this.modalVisibleSubject.next(true);
return new Promise<boolean>(resolve => {
this.resolvePromise = resolve;
});
}
accept(): void {
this.modalVisibleSubject.next(false);
this.resolvePromise(true);
}
reject(): void {
this.modalVisibleSubject.next(false);
this.resolvePromise(false);
}
}

View File

@ -46,7 +46,7 @@
<share-button button="print" showText="true"></share-button> <share-button button="print" showText="true"></share-button>
<!-- <share-button button="email" showText="true"></share-button> --> <!-- <share-button button="email" showText="true"></share-button> -->
<div class="inline"> <div class="inline">
<button class="share share-email text-white font-bold text-xs py-1.5 px-2 inline-flex items-center"> <button class="share share-email text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" (click)="showShareByEMail()">
<i class="fa-solid fa-envelope"></i> <i class="fa-solid fa-envelope"></i>
<span class="ml-2">Email</span> <span class="ml-2">Email</span>
</button> </button>
@ -56,14 +56,6 @@
<share-button button="x" showText="true"></share-button> <share-button button="x" showText="true"></share-button>
<share-button button="linkedin" showText="true"></share-button> <share-button button="linkedin" showText="true"></share-button>
</div> </div>
<!-- @if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
<button
[routerLink]="['/editBusinessListing', listing.id]"
class="w-full sm:w-auto px-4 py-2 mt-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50"
>
Edit
</button>
} -->
</div> </div>
<!-- Right column --> <!-- Right column -->
@ -77,8 +69,9 @@
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<app-validated-input label="Phone Number" name="phoneNumber" [(ngModel)]="mailinfo.sender.phoneNumber" kind="tel"></app-validated-input> <app-validated-input label="Phone Number" name="phoneNumber" [(ngModel)]="mailinfo.sender.phoneNumber" kind="tel" mask="(000) 000-0000"></app-validated-input>
<app-validated-input label="Country/State" name="state" [(ngModel)]="mailinfo.sender.state"></app-validated-input> <!-- <app-validated-input label="Country/State" name="state" [(ngModel)]="mailinfo.sender.state"></app-validated-input> -->
<app-validated-ng-select label="State" name="state" [(ngModel)]="mailinfo.sender.state" [items]="selectOptions?.states"></app-validated-ng-select>
</div> </div>
<div> <div>

View File

@ -7,8 +7,10 @@ import { lastValueFrom } from 'rxjs';
import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model'; import { KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { EMailService } from '../../../components/email/email.service';
import { MessageService } from '../../../components/message/message.service'; import { MessageService } from '../../../components/message/message.service';
import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component'; import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component';
import { ValidatedNgSelectComponent } from '../../../components/validated-ng-select/validated-ng-select.component';
import { ValidatedTextareaComponent } from '../../../components/validated-textarea/validated-textarea.component'; import { ValidatedTextareaComponent } from '../../../components/validated-textarea/validated-textarea.component';
import { ValidationMessagesService } from '../../../components/validation-messages.service'; import { ValidationMessagesService } from '../../../components/validation-messages.service';
import { HistoryService } from '../../../services/history.service'; import { HistoryService } from '../../../services/history.service';
@ -22,7 +24,7 @@ import { createMailInfo, map2User } from '../../../utils/utils';
@Component({ @Component({
selector: 'app-details-business-listing', selector: 'app-details-business-listing',
standalone: true, standalone: true,
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton], imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton, ValidatedNgSelectComponent],
providers: [], providers: [],
templateUrl: './details-business-listing.component.html', templateUrl: './details-business-listing.component.html',
styleUrl: '../details.scss', styleUrl: '../details.scss',
@ -70,6 +72,7 @@ export class DetailsBusinessListingComponent {
private validationMessagesService: ValidationMessagesService, private validationMessagesService: ValidationMessagesService,
private messageService: MessageService, private messageService: MessageService,
private logService: LogService, private logService: LogService,
public emailService: EMailService,
) { ) {
this.router.events.subscribe(event => { this.router.events.subscribe(event => {
if (event instanceof NavigationEnd) { if (event instanceof NavigationEnd) {
@ -153,4 +156,21 @@ export class DetailsBusinessListingComponent {
isAlreadyFavorite() { isAlreadyFavorite() {
return this.listing.favoritesForUser.includes(this.user.email); return this.listing.favoritesForUser.includes(this.user.email);
} }
async showShareByEMail() {
const result = await this.emailService.showShareByEMail({
email: this.user.email,
name: `${this.user.firstname} ${this.user.lastname}`,
url: environment.mailinfoUrl,
listingTitle: this.listing.title,
id: this.listing.id,
type: 'business',
});
if (result) {
this.messageService.addMessage({
severity: 'success',
text: 'Your Email has beend sent',
duration: 5000,
});
}
}
} }

View File

@ -1,102 +1,3 @@
<!-- <div class="surface-ground h-full">
<div class="px-6 py-5">
<div class="surface-card p-4 shadow-2 border-round">
<div class="flex justify-content-between align-items-center align-content-center mb-2">
<div class="font-medium text-3xl text-900 mb-3">{{ listing?.title }}</div>
@if(historyService.canGoBack){
<p-button icon="pi pi-times" [rounded]="true" severity="danger" (click)="historyService.goBack()"></p-button>
}
</div>
@if(listing){
<div class="grid">
<div class="col-12 md:col-6">
<ul class="list-none p-0 m-0 border-top-1 border-300">
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-3 font-medium flex">Description</div>
<div class="text-900 w-full md:w-9 line-height-3" [innerHTML]="description"></div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-3 font-medium">Property Category</div>
<div class="text-900 w-full md:w-9">{{ selectOptions.getCommercialProperty(listing.type) }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-3 font-medium">Located in</div>
<div class="text-900 w-full md:w-9">{{ selectOptions.getState(listing.state) }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-3 font-medium">City</div>
<div class="text-900 w-full md:w-9">{{ listing.city }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-3 font-medium">Zip Code</div>
<div class="text-900 w-full md:w-9">{{ listing.zipCode }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap surface-ground">
<div class="text-500 w-full md:w-3 font-medium">County</div>
<div class="text-900 w-full md:w-9">{{ listing.county }}</div>
</li>
<li class="flex align-items-center py-3 px-2 flex-wrap">
<div class="text-500 w-full md:w-3 font-medium">Asking Price:</div>
<div class="text-900 w-full md:w-9">{{ listing.price | currency }}</div>
</li>
</ul>
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
<button pButton pRipple label="Edit" icon="pi pi-file-edit" class="w-auto" [routerLink]="['/editCommercialPropertyListing', listing.id]"></button>
}
</div>
<div class="col-12 md:col-6">
<p-galleria [value]="listing.imageOrder" [showIndicators]="true" [showThumbnails]="false" [responsiveOptions]="responsiveOptions" [containerStyle]="{ 'max-width': '640px' }" [numVisible]="5">
<ng-template pTemplate="item" let-item>
<img src="{{ env.imageBaseUrl }}/pictures/property/{{ listing.imagePath }}/{{ listing.serialId }}/{{ item }}" style="width: 100%" />
</ng-template>
</p-galleria>
@if (mailinfo){
<div class="surface-card p-4 border-round p-fluid">
<div class="font-medium text-xl text-primary text-900 mb-3">Contact the Author of this Listing</div>
<div class="font-italic text-sm text-900 mb-5">Please include your contact info below</div>
<div class="grid formgrid p-fluid">
<div class="field mb-4 col-12 md:col-6">
<label for="name" class="font-medium text-900">Your Name</label>
<input [ngClass]="{ 'ng-invalid': containsError('name'), 'ng-dirty': containsError('name') }" id="name" type="text" pInputText [(ngModel)]="mailinfo.sender.name" />
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="email" class="font-medium text-900">Your Email</label>
<input id="email" type="text" pInputText [(ngModel)]="mailinfo.sender.email" />
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="phoneNumber" class="font-medium text-900">Phone Number</label>
<p-inputMask mask="(999) 999-9999" placeholder="(123) 456-7890" [(ngModel)]="mailinfo.sender.phoneNumber"></p-inputMask>
</div>
<div class="field mb-4 col-12 md:col-6">
<label for="state" class="font-medium text-900">Country/State</label>
<input id="state" type="text" pInputText [(ngModel)]="mailinfo.sender.state" />
</div>
<div class="surface-border border-top-1 opacity-50 mb-4 col-12"></div>
<div class="field mb-4 col-12">
<label for="notes" class="font-medium text-900">Questions/Comments</label>
<textarea id="notes" pInputTextarea [autoResize]="true" [rows]="5" [(ngModel)]="mailinfo.sender.comments"></textarea>
</div>
@if(listingUser){
<div class="surface-border mb-4 col-12 flex align-items-center">
Listing by &nbsp;<a routerLink="/details-user/{{ listingUser.id }}" class="mr-2 font-semibold">{{ listingUser.firstname }} {{ listingUser.lastname }}</a>
@if(listingUser.hasCompanyLogo){
<img src="{{ env.imageBaseUrl }}/pictures/logo/{{ listing.imagePath }}.avif?_ts={{ ts }}" class="mr-5 lg:mb-0" style="max-height: 30px; max-width: 100px" />
}
</div>
}
</div>
<button pButton pRipple label="Submit" icon="pi pi-file" class="w-auto" (click)="mail()"></button>
</div>
}
</div>
</div>
}
</div>
</div>
</div> -->
<div class="container mx-auto p-4"> <div class="container mx-auto p-4">
<div class="bg-white shadow-md rounded-lg overflow-hidden"> <div class="bg-white shadow-md rounded-lg overflow-hidden">
@if(listing){ @if(listing){
@ -104,7 +5,7 @@
<h1 class="text-3xl font-bold mb-4">{{ listing?.title }}</h1> <h1 class="text-3xl font-bold mb-4">{{ listing?.title }}</h1>
<button <button
(click)="historyService.goBack()" (click)="historyService.goBack()"
class="absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50" class="print:hidden absolute top-4 right-4 bg-red-500 text-white rounded-full w-8 h-8 flex items-center justify-center hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-opacity-50"
> >
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
@ -143,9 +44,42 @@
<div class="w-full sm:w-2/3 p-2">{{ detail.value }}</div> <div class="w-full sm:w-2/3 p-2">{{ detail.value }}</div>
</div> </div>
</div> </div>
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){ <div class="py-4 print:hidden">
@if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
<div class="inline">
<button class="share share-edit text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" [routerLink]="['/editBusinessListing', listing.id]">
<i class="fa-solid fa-floppy-disk"></i>
<span class="ml-2">Edit</span>
</button>
</div>
} @if(user){
<div class="inline">
<button class="share share-save text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" (click)="save()" [disabled]="listing.favoritesForUser.includes(user.email)">
<i class="fa-solid fa-floppy-disk"></i>
@if(listing.favoritesForUser.includes(user.email)){
<span class="ml-2">Saved ...</span>
}@else {
<span class="ml-2">Save</span>
}
</button>
</div>
}
<share-button button="print" showText="true"></share-button>
<!-- <share-button button="email" showText="true"></share-button> -->
<div class="inline">
<button class="share share-email text-white font-bold text-xs py-1.5 px-2 inline-flex items-center" (click)="showShareByEMail()">
<i class="fa-solid fa-envelope"></i>
<span class="ml-2">Email</span>
</button>
</div>
<share-button button="facebook" showText="true"></share-button>
<share-button button="x" showText="true"></share-button>
<share-button button="linkedin" showText="true"></share-button>
</div>
<!-- @if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){
<button class="mt-4 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" [routerLink]="['/editCommercialPropertyListing', listing.id]">Edit</button> <button class="mt-4 bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" [routerLink]="['/editCommercialPropertyListing', listing.id]">Edit</button>
} } -->
</div> </div>
<div class="w-full lg:w-1/2 mt-6 lg:mt-0"> <div class="w-full lg:w-1/2 mt-6 lg:mt-0">
@ -177,39 +111,18 @@
</div> </div>
} }
</div> </div>
<div class="mt-6"> <div class="mt-6 print:hidden">
<h2 class="text-xl font-semibold mb-4">Contact the Author of this Listing</h2> <h2 class="text-xl font-semibold mb-4">Contact the Author of this Listing</h2>
<p class="text-sm text-gray-600 mb-4">Please include your contact info below</p> <p class="text-sm text-gray-600 mb-4">Please include your contact info below</p>
<form class="space-y-4"> <form class="space-y-4">
<!-- <div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-1">Your Name</label>
<input type="text" id="name" name="name" [(ngModel)]="mailinfo.sender.name" class="w-full px-3 py-2 border border-gray-300 rounded-md" />
</div>
<div>
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">Your Email</label>
<input type="email" id="email" name="email" [(ngModel)]="mailinfo.sender.email" class="w-full px-3 py-2 border border-gray-300 rounded-md" />
</div>
<div>
<label for="phone" class="block text-sm font-medium text-gray-700 mb-1">Phone Number</label>
<input type="tel" id="phone" name="phone" [(ngModel)]="mailinfo.sender.phoneNumber" class="w-full px-3 py-2 border border-gray-300 rounded-md" />
</div>
<div>
<label for="location" class="block text-sm font-medium text-gray-700 mb-1">Country/State</label>
<input type="text" id="location" name="location" [(ngModel)]="mailinfo.sender.state" class="w-full px-3 py-2 border border-gray-300 rounded-md" />
</div>
</div>
<div class="mb-4">
<label for="message" class="block text-sm font-medium text-gray-700 mb-1">Questions/Comments</label>
<textarea id="message" name="message" [(ngModel)]="mailinfo.sender.comments" rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-md"></textarea>
</div> -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<app-validated-input label="Your Name" name="name" [(ngModel)]="mailinfo.sender.name"></app-validated-input> <app-validated-input label="Your Name" name="name" [(ngModel)]="mailinfo.sender.name"></app-validated-input>
<app-validated-input label="Your Email" name="email" [(ngModel)]="mailinfo.sender.email" kind="email"></app-validated-input> <app-validated-input label="Your Email" name="email" [(ngModel)]="mailinfo.sender.email" kind="email"></app-validated-input>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<app-validated-input label="Phone Number" name="phoneNumber" [(ngModel)]="mailinfo.sender.phoneNumber" kind="tel"></app-validated-input> <app-validated-input label="Phone Number" name="phoneNumber" [(ngModel)]="mailinfo.sender.phoneNumber" kind="tel" mask="(000) 000-0000"></app-validated-input>
<app-validated-input label="Country/State" name="state" [(ngModel)]="mailinfo.sender.state"></app-validated-input> <!-- <app-validated-input label="Country/State" name="state" [(ngModel)]="mailinfo.sender.state"></app-validated-input> -->
<app-validated-ng-select label="State" name="state" [(ngModel)]="mailinfo.sender.state" [items]="selectOptions?.states"></app-validated-ng-select>
</div> </div>
<div> <div>
<app-validated-textarea label="Questions/Comments" name="comments" [(ngModel)]="mailinfo.sender.comments"></app-validated-textarea> <app-validated-textarea label="Questions/Comments" name="comments" [(ngModel)]="mailinfo.sender.comments"></app-validated-textarea>

View File

@ -3,12 +3,15 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { KeycloakService } from 'keycloak-angular'; import { KeycloakService } from 'keycloak-angular';
import { ShareButton } from 'ngx-sharebuttons/button';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model';
import { CommercialPropertyListingCriteria, ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model'; import { CommercialPropertyListingCriteria, ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../../../environments/environment'; import { environment } from '../../../../environments/environment';
import { EMailService } from '../../../components/email/email.service';
import { MessageService } from '../../../components/message/message.service'; import { MessageService } from '../../../components/message/message.service';
import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component'; import { ValidatedInputComponent } from '../../../components/validated-input/validated-input.component';
import { ValidatedNgSelectComponent } from '../../../components/validated-ng-select/validated-ng-select.component';
import { ValidatedTextareaComponent } from '../../../components/validated-textarea/validated-textarea.component'; import { ValidatedTextareaComponent } from '../../../components/validated-textarea/validated-textarea.component';
import { ValidationMessagesService } from '../../../components/validation-messages.service'; import { ValidationMessagesService } from '../../../components/validation-messages.service';
import { HistoryService } from '../../../services/history.service'; import { HistoryService } from '../../../services/history.service';
@ -24,7 +27,7 @@ import { createMailInfo, map2User } from '../../../utils/utils';
@Component({ @Component({
selector: 'app-details-commercial-property-listing', selector: 'app-details-commercial-property-listing',
standalone: true, standalone: true,
imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent], imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton, ValidatedNgSelectComponent],
providers: [], providers: [],
templateUrl: './details-commercial-property-listing.component.html', templateUrl: './details-commercial-property-listing.component.html',
styleUrl: '../details.scss', styleUrl: '../details.scss',
@ -77,6 +80,7 @@ export class DetailsCommercialPropertyListingComponent {
private validationMessagesService: ValidationMessagesService, private validationMessagesService: ValidationMessagesService,
private messageService: MessageService, private messageService: MessageService,
private logService: LogService, private logService: LogService,
private emailService: EMailService,
) { ) {
this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl }; this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl };
@ -152,4 +156,28 @@ export class DetailsCommercialPropertyListingComponent {
getImageIndices(): number[] { getImageIndices(): number[] {
return this.listing && this.listing.imageOrder ? this.listing.imageOrder.slice(1).map((e, i) => i + 1) : []; return this.listing && this.listing.imageOrder ? this.listing.imageOrder.slice(1).map((e, i) => i + 1) : [];
} }
save() {
this.listing.favoritesForUser.push(this.user.email);
this.listingsService.save(this.listing, 'commercialProperty');
}
isAlreadyFavorite() {
return this.listing.favoritesForUser.includes(this.user.email);
}
async showShareByEMail() {
const result = await this.emailService.showShareByEMail({
email: this.user.email,
name: `${this.user.firstname} ${this.user.lastname}`,
url: environment.mailinfoUrl,
listingTitle: this.listing.title,
id: this.listing.id,
type: 'commercialProperty',
});
if (result) {
this.messageService.addMessage({
severity: 'success',
text: 'Your Email has beend sent',
duration: 5000,
});
}
}
} }

View File

@ -70,3 +70,10 @@ button.share {
.share-email { .share-email {
background-color: #ff961c; background-color: #ff961c;
} }
:host ::ng-deep .ng-select-container {
height: 42px !important;
border-radius: 0.5rem;
.ng-value-container .ng-input {
top: 10px;
}
}

View File

@ -43,25 +43,4 @@ export class FavoritesComponent {
const result = await Promise.all([await this.listingsService.getFavoriteListings('business'), await this.listingsService.getFavoriteListings('commercialProperty')]); const result = await Promise.all([await this.listingsService.getFavoriteListings('business'), await this.listingsService.getFavoriteListings('commercialProperty')]);
this.favorites = [...result[0], ...result[1]]; this.favorites = [...result[0], ...result[1]];
} }
// listings: Array<ListingType> = []; //dataListings as unknown as Array<BusinessListing>;
// myListings: Array<ListingType>;
// user: User;
// constructor(
// public userService: UserService,
// public keycloakService: KeycloakService,
// private listingsService: ListingsService,
// private cdRef: ChangeDetectorRef,
// public selectOptions: SelectOptionsService,
// private messageService: MessageService,
// private confirmationService: ConfirmationService,
// ) {}
// async ngOnInit() {
// // const keycloakUser = this.userService.getKeycloakUser();
// const token = await this.keycloakService.getToken();
// const keycloakUser = map2User(token);
// const email = keycloakUser.email;
// this.user = await this.userService.getByMail(email);
// const result = await Promise.all([await this.listingsService.getListingsByEmail(this.user.email, 'business'), await this.listingsService.getListingsByEmail(this.user.email, 'commercialProperty')]);
// this.myListings = [...result[0], ...result[1]];
// }
} }

View File

@ -1,6 +1,7 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { ShareByEMail } from '../../../../bizmatch-server/src/models/db.model';
import { ErrorResponse, MailInfo } from '../../../../bizmatch-server/src/models/main.model'; import { ErrorResponse, MailInfo } from '../../../../bizmatch-server/src/models/main.model';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
@ -14,4 +15,7 @@ export class MailService {
async mail(mailinfo: MailInfo): Promise<void | ErrorResponse> { async mail(mailinfo: MailInfo): Promise<void | ErrorResponse> {
return await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/mail`, mailinfo)); return await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/mail`, mailinfo));
} }
async mailToFriend(shareByEMail: ShareByEMail): Promise<void | ErrorResponse> {
return await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/mail/send2Friend`, shareByEMail));
}
} }