Adding Emailing

Adding Email Verification on Patron Form
Addressing #1 - tickets will span next to ticket form
This commit is contained in:
Tara Wilson 2024-12-18 15:59:36 -05:00
parent 1d7315e063
commit 84431ede36
12 changed files with 130 additions and 18 deletions

2
.gitignore vendored
View File

@ -47,3 +47,5 @@ testem.log
# System files # System files
.DS_Store .DS_Store
Thumbs.db Thumbs.db
source/ticketAPI/api/appsettings.Development.json
source/ticketAPI/api/appsettings.json

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -56,6 +56,11 @@ public class TicketController(
Details = eventManager.GetEvent(ticket.EventId) Details = eventManager.GetEvent(ticket.EventId)
}; };
if (!string.IsNullOrEmpty(ticket.Patron.Email))
{
}
return Ok(response); return Ok(response);
} }
catch (Exception e) catch (Exception e)

View File

@ -11,5 +11,6 @@ public static class ServiceCollectionExtensions
services.AddScoped<ITicketManager, TicketManager>(); services.AddScoped<ITicketManager, TicketManager>();
services.AddScoped<IEventManager, EventManager>(); services.AddScoped<IEventManager, EventManager>();
services.AddScoped<ISeasonManager, SeasonManager>(); services.AddScoped<ISeasonManager, SeasonManager>();
services.AddScoped<IEmailService, EmailService>();
} }
} }

View File

@ -0,0 +1,9 @@
using models.Core;
using models.Response;
namespace api.Interfaces;
public interface IEmailService
{
void SendEmail(Ticket ticket, EventDetails details);
}

View File

@ -0,0 +1,71 @@
using System.Net;
using System.Net.Mail;
using System.Text;
using api.Interfaces;
using models.Core;
using models.Response;
namespace api.Services;
public class EmailService(IConfiguration config) : IEmailService
{
public void SendEmail(Ticket ticket, EventDetails @event)
{
using var client = new SmtpClient(config.GetSection("Email:Server").Value);
client.UseDefaultCredentials = false;
var auth = new NetworkCredential(config.GetSection("Email:UserName").Value,
config.GetSection("Email:Password").Value);
client.Credentials = auth;
var from = new MailAddress(config.GetSection("Email:From").Value);
var to = new MailAddress(ticket.Patron.Email);
var emailMessage = new MailMessage(from, to);
emailMessage.ReplyToList.Add(from);
emailMessage.Subject = $"Tickets - {@event.Name}";
emailMessage.SubjectEncoding = Encoding.UTF8;
emailMessage.IsBodyHtml = true;
emailMessage.Body = GenerateTicketBody(ticket, @event);
client.Send(emailMessage);
}
private string GenerateTicketBody(Ticket ticket, EventDetails @event)
{
var sb = new StringBuilder();
const string cardStyle = "margin: 15px; padding: 5px; backdrop-filter: blur(25px) saturate(112%); -webkit-backdrop-filter: blur(25px) saturate(112%); background-color: rgba(255, 255, 255, 0.11); border-radius: 12px; border: 1px solid rgba(255, 255, 255, 0.125); width: fit-content;";
const string columnStyle = "display: flex; flex-direction: column; justify-content: center;";
const string rowStyle = "display: flex; flex-direction: row; justify-content: center; align-items: baseline;";
const string qrCodeStyle = "height: 150px; width: 150px; padding: 10px;";
const string imagePath = "Assets/pso-logo.png";
sb.AppendLine($"<div #ticket style=\"{cardStyle} {columnStyle}\">");
sb.AppendLine($"<div #talentLogo style=\"{cardStyle} {rowStyle}\">");
sb.AppendLine($"<img src=\"{imagePath}\" width=\"250\" height=\"100\" alt=\"Parma Symphony Orchestra Logo\"/>");
sb.AppendLine("</div>"); //closes #talentLogo
sb.AppendLine($"<div #qrCode style=\"{rowStyle}\">");
sb.AppendLine($"<img style=\"{qrCodeStyle}\" alt=\"QR Code\" src=\"data:image/png;base64,{ticket.QrCode}\"/>");
sb.AppendLine("</div>"); //closes #qrCode
sb.AppendLine($"<div #venue style=\"{rowStyle}\">");
sb.AppendLine($"<div style=\"{cardStyle}\">");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Talent.Name}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Name}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Description}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Date.ToString("F")}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Venue.Name}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Venue.Description}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Venue.AddressOne}</span>");
sb.AppendLine($"<span style=\"{rowStyle}\">{@event.Venue.City} {@event.Venue.State} {@event.Venue.Zip}</span>");
sb.AppendLine("</div>"); //closes #venue nested card
sb.AppendLine("</div>"); //closes #venue
sb.AppendLine("</div>"); //closes #ticket
return sb.ToString();
}
}

View File

@ -21,4 +21,8 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Assets\" />
</ItemGroup>
</Project> </Project>

View File

@ -15,7 +15,8 @@
</div> </div>
<div class="row row-space-between row-bottom-margin"> <div class="row row-space-between row-bottom-margin">
<label class="label" for="email">Email</label> <label class="label" for="email">Email</label>
<input id="email" [formControl]="email" type="text" class="input"/> <input id="email" [formControl]="email" type="email" class="input"
[ngClass]="{'invalid': email.invalid && email.dirty}"/>
</div> </div>
<div class="row row-space-between row-bottom-margin"> <div class="row row-space-between row-bottom-margin">
<label class="label" for="phoneNumber">Phone</label> <label class="label" for="phoneNumber">Phone</label>

View File

@ -1,11 +1,13 @@
import {Component} from '@angular/core'; import {Component} from '@angular/core';
import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms'; import {FormControl, ReactiveFormsModule, Validators} from '@angular/forms';
import {Patron} from '../../../models/core/patron'; import {Patron} from '../../../models/core/patron';
import {NgClass} from '@angular/common';
@Component({ @Component({
selector: 'app-patron-info', selector: 'app-patron-info',
imports: [ imports: [
ReactiveFormsModule ReactiveFormsModule,
NgClass
], ],
templateUrl: './patron-info.component.html', templateUrl: './patron-info.component.html',
styleUrl: './patron-info.component.scss' styleUrl: './patron-info.component.scss'
@ -14,7 +16,9 @@ export class PatronInfoComponent {
public firstName = new FormControl('', {nonNullable: true, validators: [Validators.required]}); public firstName = new FormControl('', {nonNullable: true, validators: [Validators.required]});
public middleName = new FormControl(''); public middleName = new FormControl('');
public lastName = new FormControl('', {nonNullable: true, validators: [Validators.required]}); public lastName = new FormControl('', {nonNullable: true, validators: [Validators.required]});
public email = new FormControl('', {nonNullable: true, validators: [Validators.required]}); public email = new FormControl('', {
nonNullable: true, validators: [Validators.required, Validators.email]
});
public phoneNumber = new FormControl(''); public phoneNumber = new FormControl('');
public addressOne = new FormControl('', {nonNullable: true, validators: [Validators.required]}); public addressOne = new FormControl('', {nonNullable: true, validators: [Validators.required]});
public addressTwo = new FormControl(''); public addressTwo = new FormControl('');

View File

@ -1,16 +1,18 @@
<div class="card"> <div [ngClass]="{'row': ticket$() !== null}">
<h3>Generate Tickets</h3> <div class="card">
<div class="column"> <h3>Generate Tickets</h3>
<app-ticket-type-selector #ticketType></app-ticket-type-selector> <div class="column">
<app-event-browser /> <app-ticket-type-selector #ticketType></app-ticket-type-selector>
@if(IsSeasonTicket(ticketType.selectedTicketType$())) { <app-event-browser />
<app-season-browser /> @if(IsSeasonTicket(ticketType.selectedTicketType$())) {
} <app-season-browser />
<app-patron-info /> }
<app-patron-info />
</div>
<button class="button" (click)="saveTicket()">Save Ticket</button>
</div> </div>
<button class="button" (click)="saveTicket()">Save Ticket</button>
</div>
@if(ticket$() !== null) { @if(ticket$() !== null) {
<app-generated-ticket /> <app-generated-ticket />
} }
</div>

View File

@ -8,6 +8,8 @@ import {SeasonBrowserComponent} from '../../components/season-browser/season-bro
import {IsSeasonTicket} from '../../../models/enums/ticket-type.enum'; import {IsSeasonTicket} from '../../../models/enums/ticket-type.enum';
import {GeneratedTicketComponent} from '../../components/generated-ticket/generated-ticket.component'; import {GeneratedTicketComponent} from '../../components/generated-ticket/generated-ticket.component';
import {Guid} from 'guid-typescript'; import {Guid} from 'guid-typescript';
import {SnackbarService} from '../../services/snackbar.service';
import {NgClass} from '@angular/common';
@Component({ @Component({
selector: 'app-ticket', selector: 'app-ticket',
@ -16,7 +18,8 @@ import {Guid} from 'guid-typescript';
TicketTypeSelectorComponent, TicketTypeSelectorComponent,
PatronInfoComponent, PatronInfoComponent,
SeasonBrowserComponent, SeasonBrowserComponent,
GeneratedTicketComponent GeneratedTicketComponent,
NgClass
], ],
templateUrl: './ticket.component.html', templateUrl: './ticket.component.html',
styleUrl: './ticket.component.scss' styleUrl: './ticket.component.scss'
@ -28,9 +31,15 @@ export class TicketComponent {
@ViewChild(SeasonBrowserComponent) seasonBrowserComponent!: SeasonBrowserComponent; @ViewChild(SeasonBrowserComponent) seasonBrowserComponent!: SeasonBrowserComponent;
public ticketService = inject(TicketService); public ticketService = inject(TicketService);
public snackBar = inject(SnackbarService);
public ticket$ = this.ticketService.mintResponse$; public ticket$ = this.ticketService.mintResponse$;
public saveTicket(): void { public saveTicket(): void {
if(this.patronInfoComponent.email.invalid && this.patronInfoComponent.email.dirty) {
this.snackBar.showMessage("Please enter a valid email address");
return;
}
const addTicket: AddTicket = { const addTicket: AddTicket = {
eventId: this.eventBrowserComponent.selectedEventId, eventId: this.eventBrowserComponent.selectedEventId,
type: this.ticketTypeSelector.selectedTicketType, type: this.ticketTypeSelector.selectedTicketType,

View File

@ -99,3 +99,7 @@ body {
justify-content: space-between; justify-content: space-between;
} }
} }
.invalid {
border: 1px solid rgba(255, 43, 43, 0.5);
}