diff --git a/bizmatch-server/src/mail/mail.controller.ts b/bizmatch-server/src/mail/mail.controller.ts index 20c3a3a..27a43b8 100644 --- a/bizmatch-server/src/mail/mail.controller.ts +++ b/bizmatch-server/src/mail/mail.controller.ts @@ -1,5 +1,5 @@ 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 { MailService } from './mail.service.js'; @@ -18,4 +18,8 @@ export class MailController { sendSubscriptionConfirmation(@Body() user: User): Promise { return this.mailService.sendSubscriptionConfirmation(user); } + @Post('send2Friend') + send2Friend(@Body() shareByEMail: ShareByEMail): Promise { + return this.mailService.send2Friend(shareByEMail); + } } diff --git a/bizmatch-server/src/mail/mail.service.ts b/bizmatch-server/src/mail/mail.service.ts index 4d3337e..19ae0e7 100644 --- a/bizmatch-server/src/mail/mail.service.ts +++ b/bizmatch-server/src/mail/mail.service.ts @@ -3,7 +3,7 @@ import { BadRequestException, Injectable } from '@nestjs/common'; import path, { join } from 'path'; import { fileURLToPath } from 'url'; 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 { UserService } from '../user/user.service.js'; const __filename = fileURLToPath(import.meta.url); @@ -96,4 +96,33 @@ export class MailService { }, }); } + async send2Friend(shareByEMail: ShareByEMail): Promise { + 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" `, + 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, + }, + }); + } } diff --git a/bizmatch-server/src/mail/templates/send2Friend.hbs b/bizmatch-server/src/mail/templates/send2Friend.hbs new file mode 100644 index 0000000..1c10542 --- /dev/null +++ b/bizmatch-server/src/mail/templates/send2Friend.hbs @@ -0,0 +1,73 @@ + + + + + + Notification + + + + + + \ No newline at end of file diff --git a/bizmatch-server/src/models/db.model.ts b/bizmatch-server/src/models/db.model.ts index f644d0e..c338834 100644 --- a/bizmatch-server/src/models/db.model.ts +++ b/bizmatch-server/src/models/db.model.ts @@ -301,3 +301,13 @@ export const SenderSchema = z.object({ comments: z.string().min(10, { message: 'Comments must be at least 10 characters long' }), }); export type Sender = z.infer; +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; diff --git a/bizmatch/src/app/app.component.html b/bizmatch/src/app/app.component.html index 52ea5a1..3bd57da 100644 --- a/bizmatch/src/app/app.component.html +++ b/bizmatch/src/app/app.component.html @@ -36,3 +36,4 @@ + diff --git a/bizmatch/src/app/app.component.ts b/bizmatch/src/app/app.component.ts index b5873b0..abaad0b 100644 --- a/bizmatch/src/app/app.component.ts +++ b/bizmatch/src/app/app.component.ts @@ -7,6 +7,7 @@ import { filter } from 'rxjs/operators'; import build from '../build'; import { ConfirmationComponent } from './components/confirmation/confirmation.component'; import { ConfirmationService } from './components/confirmation/confirmation.service'; +import { EMailComponent } from './components/email/email.component'; import { FooterComponent } from './components/footer/footer.component'; import { HeaderComponent } from './components/header/header.component'; import { MessageContainerComponent } from './components/message/message-container.component'; @@ -17,7 +18,7 @@ import { UserService } from './services/user.service'; @Component({ selector: 'app-root', standalone: true, - imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent], + imports: [CommonModule, RouterOutlet, HeaderComponent, FooterComponent, MessageContainerComponent, SearchModalComponent, ConfirmationComponent, EMailComponent], providers: [], templateUrl: './app.component.html', styleUrl: './app.component.scss', diff --git a/bizmatch/src/app/components/email/email.component.html b/bizmatch/src/app/components/email/email.component.html new file mode 100644 index 0000000..9df7a17 --- /dev/null +++ b/bizmatch/src/app/components/email/email.component.html @@ -0,0 +1,43 @@ + +
+
+ +
+ +
+

Email listing to a friend

+ +
+ +
+
+
+ +
+
+ +
+
+ +
+ + +
+
+
+
+
diff --git a/bizmatch/src/app/components/email/email.component.ts b/bizmatch/src/app/components/email/email.component.ts new file mode 100644 index 0000000..73cc57e --- /dev/null +++ b/bizmatch/src/app/components/email/email.component.ts @@ -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 + } +} diff --git a/bizmatch/src/app/components/email/email.service.ts b/bizmatch/src/app/components/email/email.service.ts new file mode 100644 index 0000000..bfc0786 --- /dev/null +++ b/bizmatch/src/app/components/email/email.service.ts @@ -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(false); + private shareByEMailSubject = new BehaviorSubject({}); + private resolvePromise!: (value: boolean) => void; + + modalVisible$: Observable = this.modalVisibleSubject.asObservable(); + shareByEMail$: Observable = this.shareByEMailSubject.asObservable(); + + showShareByEMail(shareByEMail: ShareByEMail): Promise { + this.shareByEMailSubject.next(shareByEMail); + this.modalVisibleSubject.next(true); + return new Promise(resolve => { + this.resolvePromise = resolve; + }); + } + + accept(): void { + this.modalVisibleSubject.next(false); + this.resolvePromise(true); + } + + reject(): void { + this.modalVisibleSubject.next(false); + this.resolvePromise(false); + } +} diff --git a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.html b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.html index d77abd5..660950e 100644 --- a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.html +++ b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.html @@ -46,7 +46,7 @@
- @@ -56,14 +56,6 @@
- @@ -77,8 +69,9 @@
- - + + +
diff --git a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts index c5c9f00..f98810c 100644 --- a/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts +++ b/bizmatch/src/app/pages/details/details-business-listing/details-business-listing.component.ts @@ -7,8 +7,10 @@ import { lastValueFrom } from 'rxjs'; import { BusinessListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model'; import { environment } from '../../../../environments/environment'; +import { EMailService } from '../../../components/email/email.service'; import { MessageService } from '../../../components/message/message.service'; 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 { ValidationMessagesService } from '../../../components/validation-messages.service'; import { HistoryService } from '../../../services/history.service'; @@ -22,7 +24,7 @@ import { createMailInfo, map2User } from '../../../utils/utils'; @Component({ selector: 'app-details-business-listing', standalone: true, - imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton], + imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton, ValidatedNgSelectComponent], providers: [], templateUrl: './details-business-listing.component.html', styleUrl: '../details.scss', @@ -70,6 +72,7 @@ export class DetailsBusinessListingComponent { private validationMessagesService: ValidationMessagesService, private messageService: MessageService, private logService: LogService, + public emailService: EMailService, ) { this.router.events.subscribe(event => { if (event instanceof NavigationEnd) { @@ -153,4 +156,21 @@ export class DetailsBusinessListingComponent { 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: 'business', + }); + if (result) { + this.messageService.addMessage({ + severity: 'success', + text: 'Your Email has beend sent', + duration: 5000, + }); + } + } } diff --git a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.html b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.html index 54385e0..46ab77b 100644 --- a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.html +++ b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.html @@ -1,102 +1,3 @@ - -
@if(listing){ @@ -104,7 +5,7 @@

{{ listing?.title }}

@@ -143,9 +44,42 @@
{{ detail.value }}
- @if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){ +
+ @if(listing && listingUser && (listingUser?.email===user?.email || isAdmin())){ +
+ +
+ } @if(user){ +
+ +
+ } + + +
+ +
+ + + + +
+
@@ -177,39 +111,18 @@
} -
+

Contact the Author of this Listing

Please include your contact info below

-
- - + + +
diff --git a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts index 2bf5989..d9e132f 100644 --- a/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts +++ b/bizmatch/src/app/pages/details/details-commercial-property-listing/details-commercial-property-listing.component.ts @@ -3,12 +3,15 @@ import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; import { faTimes } from '@fortawesome/free-solid-svg-icons'; import { KeycloakService } from 'keycloak-angular'; +import { ShareButton } from 'ngx-sharebuttons/button'; import { lastValueFrom } from 'rxjs'; import { CommercialPropertyListing, User } from '../../../../../../bizmatch-server/src/models/db.model'; import { CommercialPropertyListingCriteria, ErrorResponse, KeycloakUser, MailInfo } from '../../../../../../bizmatch-server/src/models/main.model'; import { environment } from '../../../../environments/environment'; +import { EMailService } from '../../../components/email/email.service'; import { MessageService } from '../../../components/message/message.service'; 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 { ValidationMessagesService } from '../../../components/validation-messages.service'; import { HistoryService } from '../../../services/history.service'; @@ -24,7 +27,7 @@ import { createMailInfo, map2User } from '../../../utils/utils'; @Component({ selector: 'app-details-commercial-property-listing', standalone: true, - imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent], + imports: [SharedModule, ValidatedInputComponent, ValidatedTextareaComponent, ShareButton, ValidatedNgSelectComponent], providers: [], templateUrl: './details-commercial-property-listing.component.html', styleUrl: '../details.scss', @@ -77,6 +80,7 @@ export class DetailsCommercialPropertyListingComponent { private validationMessagesService: ValidationMessagesService, private messageService: MessageService, private logService: LogService, + private emailService: EMailService, ) { this.mailinfo = { sender: {}, email: '', url: environment.mailinfoUrl }; @@ -152,4 +156,28 @@ export class DetailsCommercialPropertyListingComponent { getImageIndices(): number[] { 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, + }); + } + } } diff --git a/bizmatch/src/app/pages/details/details.scss b/bizmatch/src/app/pages/details/details.scss index 9d5f8a4..3273eaf 100644 --- a/bizmatch/src/app/pages/details/details.scss +++ b/bizmatch/src/app/pages/details/details.scss @@ -70,3 +70,10 @@ button.share { .share-email { background-color: #ff961c; } +:host ::ng-deep .ng-select-container { + height: 42px !important; + border-radius: 0.5rem; + .ng-value-container .ng-input { + top: 10px; + } +} diff --git a/bizmatch/src/app/pages/subscription/favorites/favorites.component.ts b/bizmatch/src/app/pages/subscription/favorites/favorites.component.ts index 4f0a238..4bc6464 100644 --- a/bizmatch/src/app/pages/subscription/favorites/favorites.component.ts +++ b/bizmatch/src/app/pages/subscription/favorites/favorites.component.ts @@ -43,25 +43,4 @@ export class FavoritesComponent { const result = await Promise.all([await this.listingsService.getFavoriteListings('business'), await this.listingsService.getFavoriteListings('commercialProperty')]); this.favorites = [...result[0], ...result[1]]; } - // listings: Array = []; //dataListings as unknown as Array; - // myListings: Array; - // 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]]; - // } } diff --git a/bizmatch/src/app/services/mail.service.ts b/bizmatch/src/app/services/mail.service.ts index 33ffed1..71617ba 100644 --- a/bizmatch/src/app/services/mail.service.ts +++ b/bizmatch/src/app/services/mail.service.ts @@ -1,6 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { lastValueFrom } from 'rxjs'; +import { ShareByEMail } from '../../../../bizmatch-server/src/models/db.model'; import { ErrorResponse, MailInfo } from '../../../../bizmatch-server/src/models/main.model'; import { environment } from '../../environments/environment'; @@ -14,4 +15,7 @@ export class MailService { async mail(mailinfo: MailInfo): Promise { return await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/mail`, mailinfo)); } + async mailToFriend(shareByEMail: ShareByEMail): Promise { + return await lastValueFrom(this.http.post(`${this.apiBaseUrl}/bizmatch/mail/send2Friend`, shareByEMail)); + } }