package templates import ( "fmt" "net/url" "prosocial/database" "prosocial/environment" "time" _ "time/tzdata" "github.com/rs/zerolog/log" "github.com/google/uuid" ) type Template interface { TemplatePath() string // path to the template stored in the server OnClickPath() string // every notification needs to be clickable, and that should take the user somewhere. // leave blank to mark as read?? } type welcome struct { UserID uuid.UUID FirstName string LastName string Date time.Time } func Welcome(userId uuid.UUID, firstName string, lastName string) welcome { return welcome{ UserID: userId, FirstName: firstName, LastName: lastName, Date: time.Now().UTC().Local(), } } func (m welcome) TemplatePath() string { return "templates/web/welcome.tmpl" } func (m welcome) OnClickPath() string { return "/" } type organizationDonation struct { DonationID string UserID uuid.UUID FullName string Organization string Amount string } func OrganizationDonation(donationId string, userId uuid.UUID, fullName, organizationName string) organizationDonation { return organizationDonation{ DonationID: donationId, UserID: userId, FullName: fullName, Organization: organizationName, } } func (m organizationDonation) TemplatePath() string { return "templates/web/organization-donation.tmpl" } func (m organizationDonation) OnClickPath() string { return fmt.Sprintf("/profile/%s/posts?highlight=%s", m.UserID, m.DonationID) } type newFollower struct { UserID uuid.UUID FullName string } func NewFollower(userId uuid.UUID, fullName string) newFollower { return newFollower{ UserID: userId, FullName: fullName, } } func (m newFollower) TemplatePath() string { return "templates/web/new-follower.tmpl" } func (m newFollower) OnClickPath() string { return fmt.Sprintf("/profile/%s", m.UserID) } type newOrganizationFollower struct { UserID uuid.UUID FullName string Organization string } func NewOrganizationFollower(userId uuid.UUID, fullName, organization string) newOrganizationFollower { return newOrganizationFollower{ UserID: userId, FullName: fullName, Organization: organization, } } func (m newOrganizationFollower) TemplatePath() string { return "templates/web/new-follower-org.tmpl" } func (m newOrganizationFollower) OnClickPath() string { return fmt.Sprintf("/profile/%s", m.UserID) } type followerDonation struct { PostID string UserID uuid.UUID FullName string Organization string Amount string } func FollowerDonation(postId string, userId uuid.UUID, fullName string) followerDonation { return followerDonation{ PostID: postId, UserID: userId, FullName: fullName, } } func (m followerDonation) TemplatePath() string { return "templates/web/follower-donation.tmpl" } func (m followerDonation) OnClickPath() string { return fmt.Sprintf("/profile/%s/posts?highlight=%s", m.UserID, m.PostID) } type verificationEmail struct { email string Recipient string VerificationCode string Date string OperatingSystem string Browser string Location string } type passwordResetEmail struct { email string Recipient string ResetLink string Date string OperatingSystem string Browser string Location string } func NewVerificationEmail(recipient, verificationCode, operatingSystem, browser string) verificationEmail { return verificationEmail{ email: recipient, Recipient: getNameByEmail(recipient), VerificationCode: verificationCode, Date: time.Now().Format("2006-01-02 15:04"), OperatingSystem: operatingSystem, Browser: browser, } } func NewPasswordResetEmail(recipient, passwordResetLink, operatingSystem, browser string) passwordResetEmail { return passwordResetEmail{ email: recipient, Recipient: getNameByEmail(recipient), ResetLink: passwordResetLink, Date: time.Now().Format("2006-01-02 15:04"), OperatingSystem: operatingSystem, Browser: browser, } } func (m verificationEmail) TemplatePath() string { return "templates/email/verification-code.tmpl" } func (m verificationEmail) OnClickPath() string { return "" } func (m passwordResetEmail) TemplatePath() string { return "templates/email/password-reset-token.tmpl" } func getNameByEmail(email string) string { user, err := database.GetUserByEmail(email) if err != nil { log.Debug().Err(err) return "Failed to retrieve user." } return user.Name } type crowdfundDonation struct { PostID string UserID uuid.UUID FullName string } func CrowdfundDonation(postId string, userId uuid.UUID, fullName string) crowdfundDonation { return crowdfundDonation{ PostID: postId, UserID: userId, FullName: fullName, } } func (m crowdfundDonation) TemplatePath() string { return "templates/web/crowdfund-donation.tmpl" } func (m crowdfundDonation) OnClickPath() string { return fmt.Sprintf("/profile/%s/posts?highlight=%s", m.UserID, m.PostID) } type organizationPost struct { OrgId string Name string PostType string PostId int64 } func OrganizationPost(orgId, name, postType string, postId int64) organizationPost { return organizationPost{ OrgId: orgId, Name: name, PostType: postType, PostId: postId, } } func (m organizationPost) TemplatePath() string { return fmt.Sprintf("templates/web/organization-%s.tmpl", m.PostType) } func (m organizationPost) OnClickPath() string { return fmt.Sprintf("/organizations/%s/posts?highlight=%d", m.OrgId, m.PostId) } type organizationInviteEmail struct { RecipientEmail string RecipientName string OrganizationName string InviterName string Role string Date string AcceptInvitationLink string } // NewOrganizationInviteEmail creates a new instance of the organizationInviteEmail template func NewOrganizationInviteEmail(recipientEmail, organizationName, inviterName, role, token string) organizationInviteEmail { return organizationInviteEmail{ RecipientEmail: recipientEmail, OrganizationName: organizationName, InviterName: inviterName, Role: role, Date: time.Now().Format("2006-01-02 15:04"), AcceptInvitationLink: fmt.Sprintf("%s/organizations/invite/accept?token=%s", environment.FrontEndURL, token), } } // TemplatePath returns the file path to the organization invite email template func (m organizationInviteEmail) TemplatePath() string { return "templates/email/invite-to-manage-org.tmpl" } // OnClickPath returns the URL the recipient should click to accept the invitation func (m organizationInviteEmail) OnClickPath() string { return "" } type organizationInviteNotification struct { OrganizationName string OrganizationLogoUrl string InviterName string Role string Date string AcceptInvitationLink string } // NewOrganizationInviteEmail creates a new instance of the organizationInviteEmail template func NewOrganizationInviteNotification(organizationName, organizationLogoUrl, inviterName, role, token string) organizationInviteNotification { return organizationInviteNotification{ OrganizationName: organizationName, OrganizationLogoUrl: organizationLogoUrl, InviterName: inviterName, Role: role, AcceptInvitationLink: fmt.Sprintf("%s/organizations/invite/accept?token=%s", environment.FrontEndURL, token), } } // TemplatePath returns the file path to the organization invite email template func (m organizationInviteNotification) TemplatePath() string { return "templates/web/invite-to-manage-org.tmpl" } // OnClickPath returns the URL the recipient should click to accept the invitation func (m organizationInviteNotification) OnClickPath() string { return m.AcceptInvitationLink } type donationReceiptEmail struct { RecipientName string DonationAmount string OrganizationName string OrganizationURL string TaxID string ReceiptURL string ReceiptCenterURL string DonationDate string SupportEmail string } // NewDonationReceiptEmail creates a new instance of the donation receipt email template. func NewDonationReceiptEmail(user database.User, organization database.Organization, receiptURL string, donation database.Donation) donationReceiptEmail { var donationDate = donation.DonationDate parsedDate, err := time.Parse(time.RFC3339, donationDate) if err != nil { log.Debug().Err(err).Msgf("Failed to parse donation date") } else { donationDate = parsedDate.Format("January 2, 2006") } return donationReceiptEmail{ RecipientName: user.GivenName, DonationAmount: fmt.Sprintf("$%.2f", donation.Amount), OrganizationName: organization.Name, TaxID: organization.TaxID, OrganizationURL: fmt.Sprintf("%s/organizations/%s", environment.FrontEndURL, organization.Slug), ReceiptURL: receiptURL, ReceiptCenterURL: fmt.Sprintf("%s/settings/receipts", environment.FrontEndURL), DonationDate: donationDate, SupportEmail: "support@giveasy.co", } } // TemplatePath returns the file path to the donation receipt email template. func (m donationReceiptEmail) TemplatePath() string { return "templates/email/donation-receipt.tmpl" } type donationReceiptEmailForOrganization struct { RecipientName string UserProfileURL string DonationAmount string OrganizationName string DashboardURL string ReceiptURL string DonationDate string SupportEmail string } // NewDonationReceiptEmail creates a new instance of the donation receipt email template. func NewDonationReceiptEmailForOrganization(user database.User, organization database.Organization, receiptURL string, donation database.Donation) donationReceiptEmailForOrganization { var donationDate = donation.DonationDate parsedDate, err := time.Parse(time.RFC3339, donationDate) if err != nil { log.Debug().Err(err).Msgf("Failed to parse donation date") } else { donationDate = parsedDate.Format("January 2, 2006") } return donationReceiptEmailForOrganization{ RecipientName: user.Name, UserProfileURL: fmt.Sprintf("%s/profile/%s", environment.FrontEndURL, user.UUID), DonationAmount: fmt.Sprintf("$%.2f", donation.Amount), OrganizationName: organization.Name, DashboardURL: fmt.Sprintf("%s/organization-dashboard/%s", environment.FrontEndURL, organization.OrganizationID), ReceiptURL: receiptURL, DonationDate: donationDate, SupportEmail: "support@giveasy.co", } } // TemplatePath returns the file path to the donation receipt email template. func (m donationReceiptEmailForOrganization) TemplatePath() string { return "templates/email/donation-receipt-org.tmpl" } type volunteeringApproved struct { PostID string UserID uuid.UUID OrganizationName string Hours float64 } func VolunteeringApproved(postId string, userId uuid.UUID, organizationName string, hours float64) volunteeringApproved { return volunteeringApproved{ PostID: postId, UserID: userId, OrganizationName: organizationName, Hours: hours, } } func (m volunteeringApproved) TemplatePath() string { return "templates/web/volunteering-approved.tmpl" } func (m volunteeringApproved) OnClickPath() string { return fmt.Sprintf("/profile/%s/posts?highlight=%s&update=%s", m.UserID, m.PostID, m.PostID) } type registrationConfirmationEmail struct { RecipientName string Organization string EventTitle string EventDate string EventTime string StreetAddress *string Apt *string City *string State *string Zipcode *string VirtualMeetingURL *string MeetingInstructions *string EventLink string CalendarLink string MapsLink string } func NewRegistrationConfirmationEmail( recipientName, organization, startTime, endTime, eventTitle, eventId string, streetAddress, apt, city, state, zipcode, virtualMeetingURL, meetingInstructions *string, timeZone *string, ) registrationConfirmationEmail { // Parse startTime and endTime (assumed to be RFC3339) startTimeDate, startTimeErr := time.Parse(time.RFC3339, startTime) endTimeDate, endTimeErr := time.Parse(time.RFC3339, endTime) var formattedDate, formattedTime, startGoogle, endGoogle string if startTimeErr != nil || endTimeErr != nil { log.Debug().Err(startTimeErr).Err(endTimeErr).Msg("Failed to parse start or end time") formattedDate = startTime formattedTime = startTime startGoogle = url.QueryEscape(startTime) endGoogle = url.QueryEscape(endTime) } else { // Default to UTC localTime := startTimeDate // Try to convert to the specified time zone if provided if timeZone != nil && *timeZone != "" { loc, err := time.LoadLocation(*timeZone) if err == nil { localTime = startTimeDate.In(loc) } else { log.Debug().Err(err).Str("timezone", *timeZone).Msg("Failed to load provided time zone, falling back to America/New_York") // Fall back to America/New_York locNY, errNY := time.LoadLocation("America/New_York") if errNY == nil { localTime = startTimeDate.In(locNY) } else { // If still failing, use a fixed -5 hours offset (approximate EST) log.Debug().Err(errNY).Msg("Failed to load America/New_York time zone, using UTC-5 approximation") localTime = startTimeDate.UTC().Add(-5 * time.Hour) } } } else { // If no time zone specified, try EST/EDT as default locNY, errNY := time.LoadLocation("America/New_York") if errNY == nil { localTime = startTimeDate.In(locNY) } else { // If time zone database is unavailable, use a fixed offset approximation for EST (-5 hours) log.Debug().Err(errNY).Msg("Failed to load America/New_York time zone, using UTC-5 approximation") localTime = startTimeDate.UTC().Add(-5 * time.Hour) } } formattedDate = localTime.Format("Monday, January 2, 2006") formattedTime = localTime.Format("3:04 PM") startGoogle = startTimeDate.UTC().Format("20060102T150405Z") endGoogle = endTimeDate.UTC().Format("20060102T150405Z") } // Rest of the function remains the same // Build details string for Google Calendar details := fmt.Sprintf("Registration confirmed for event %s organized by %s.", eventTitle, organization) // Determine event location for the calendar link var calendarLocation string if virtualMeetingURL != nil && *virtualMeetingURL != "" { calendarLocation = *virtualMeetingURL } else if streetAddress != nil && *streetAddress != "" { calendarLocation = *streetAddress if apt != nil && *apt != "" { calendarLocation += ", " + *apt } if city != nil && *city != "" { calendarLocation += ", " + *city } if state != nil && *state != "" { calendarLocation += ", " + *state } if zipcode != nil && *zipcode != "" { calendarLocation += " " + *zipcode } } // Generate Google Calendar link calendarLink := fmt.Sprintf( "https://calendar.google.com/calendar/render?action=TEMPLATE&text=%s&dates=%s/%s&details=%s&location=%s", url.QueryEscape(eventTitle), startGoogle, endGoogle, url.QueryEscape(details), url.QueryEscape(calendarLocation), ) // Generate Maps link if a physical address is provided var mapsLink string if streetAddress != nil && *streetAddress != "" { fullAddress := *streetAddress if apt != nil && *apt != "" { fullAddress += ", " + *apt } if city != nil && *city != "" { fullAddress += ", " + *city } if state != nil && *state != "" { fullAddress += ", " + *state } if zipcode != nil && *zipcode != "" { fullAddress += " " + *zipcode } mapsLink = fmt.Sprintf("https://www.google.com/maps/search/?api=1&query=%s", url.QueryEscape(fullAddress)) } return registrationConfirmationEmail{ RecipientName: recipientName, Organization: organization, EventTitle: eventTitle, EventDate: formattedDate, EventTime: formattedTime, StreetAddress: streetAddress, Apt: apt, City: city, State: state, Zipcode: zipcode, VirtualMeetingURL: virtualMeetingURL, MeetingInstructions: meetingInstructions, EventLink: fmt.Sprintf("%s/volunteer/%s", environment.FrontEndURL, eventId), CalendarLink: calendarLink, MapsLink: mapsLink, } } func (m registrationConfirmationEmail) TemplatePath() string { return "templates/email/registration-confirmation.tmpl" } func (m registrationConfirmationEmail) OnClickPath() string { return m.EventLink } type eventRegistrationEmailToOrganization struct { RegisteringUser string RegisteringUserSlug string RegisteringUserURL string Organization string OrganizationId string EventTitle string EventDate string EventTime string EventLink string DashboardLink string } func NewEventRegistrationEmailToOrganization(registeringUser, registeringUserSlug, organization, startTime, endTime, eventTitle, eventId, organizationId string) eventRegistrationEmailToOrganization { // Parse startTime (assumed to be RFC3339) startTimeDate, startTimeErr := time.Parse(time.RFC3339, startTime) var formattedDate, formattedTime string if startTimeErr != nil { log.Debug().Err(startTimeErr).Msg("Failed to parse start time") formattedDate = startTime formattedTime = startTime } else { // Default to America/New_York timezone locNY, errNY := time.LoadLocation("America/New_York") localTime := startTimeDate if errNY == nil { localTime = startTimeDate.In(locNY) } else { // If time zone database is unavailable, use a fixed offset approximation for EST (-5 hours) log.Debug().Err(errNY).Msg("Failed to load America/New_York time zone, using UTC-5 approximation") localTime = startTimeDate.UTC().Add(-5 * time.Hour) } formattedDate = localTime.Format("Monday, January 2, 2006") formattedTime = localTime.Format("3:04 PM") } return eventRegistrationEmailToOrganization{ RegisteringUser: registeringUser, RegisteringUserSlug: registeringUserSlug, RegisteringUserURL: fmt.Sprintf("%s/profile/%s", environment.FrontEndURL, registeringUserSlug), Organization: organization, OrganizationId: organizationId, EventTitle: eventTitle, EventDate: formattedDate, EventTime: formattedTime, EventLink: fmt.Sprintf("%s/volunteer/%s", environment.FrontEndURL, eventId), DashboardLink: fmt.Sprintf("%s/organization-dashboard/%s/volunteers", environment.FrontEndURL, organizationId), } } func (m eventRegistrationEmailToOrganization) TemplatePath() string { return "templates/email/event-registration-to-organization.tmpl" } func (m eventRegistrationEmailToOrganization) OnClickPath() string { return "" } type eventDetailsUpdated struct { OrganizationName string EventId string EventName string } func EventDetailsUpdated(organizationName, eventId, eventName string) eventDetailsUpdated { return eventDetailsUpdated{ OrganizationName: organizationName, EventId: eventId, EventName: eventName, } } func (m eventDetailsUpdated) TemplatePath() string { return "templates/web/event-details-updated.tmpl" } func (m eventDetailsUpdated) OnClickPath() string { return fmt.Sprintf("/volunteer/%s", m.EventId) } type eventDetailsUpdatedEmail struct { OrganizationName string OrganizationURL string EventId string EventName string RecipientName string EventLink string } func EventDetailsUpdatedEmail(organization database.Organization, eventId, eventName, recipientName string) eventDetailsUpdatedEmail { return eventDetailsUpdatedEmail{ OrganizationName: organization.Name, OrganizationURL: fmt.Sprintf("%s/organizations/%s", environment.FrontEndURL, organization.Slug), EventId: eventId, EventName: eventName, RecipientName: recipientName, EventLink: fmt.Sprintf("%s/volunteer/%s", environment.FrontEndURL, eventId), } } func (m eventDetailsUpdatedEmail) TemplatePath() string { return "templates/email/event-details-updated.tmpl" } func (m eventDetailsUpdatedEmail) OnClickPath() string { return "" } type sessionCanceled struct { OrganizationName string EventId string EventName string } func SessionCanceled(organizationName, eventId, eventName string) sessionCanceled { return sessionCanceled{ OrganizationName: organizationName, EventId: eventId, EventName: eventName, } } func (m sessionCanceled) TemplatePath() string { return "templates/web/session-canceled.tmpl" } func (m sessionCanceled) OnClickPath() string { return fmt.Sprintf("/volunteer/%s", m.EventId) } type sessionCanceledEmail struct { RecipientName string Organization string EventName string StartDate string StartTime string EndTime string EventLocation string EventLink string } func NewSessionCanceledEmail(recipientName, organization, eventId, eventName, startTime, endTime string) sessionCanceledEmail { startTimeDate, err := time.Parse(time.RFC3339, startTime) if err != nil { log.Debug().Err(err).Msg("Failed to parse start time") return sessionCanceledEmail{ RecipientName: recipientName, Organization: organization, EventName: eventName, StartDate: startTime, StartTime: startTime, EndTime: endTime, EventLink: fmt.Sprintf("%s/volunteer/%s", environment.FrontEndURL, eventId), } } return sessionCanceledEmail{ RecipientName: recipientName, Organization: organization, EventName: eventName, StartDate: startTimeDate.Format("Monday, January 2, 2006"), StartTime: startTimeDate.Format("3:04 PM"), EndTime: endTime, EventLink: fmt.Sprintf("%s/volunteer/%s", environment.FrontEndURL, eventId), } } func (m sessionCanceledEmail) TemplatePath() string { return "templates/email/session-canceled.tmpl" } func (m sessionCanceledEmail) OnClickPath() string { return m.EventLink } type taskApproved struct { PostID string UserID uuid.UUID OrganizationName string Hours float64 } func TaskApproved(postId string, userId uuid.UUID, organizationName string, hours float64) taskApproved { return taskApproved{ PostID: postId, UserID: userId, OrganizationName: organizationName, Hours: hours, } } func (m taskApproved) TemplatePath() string { return "templates/web/task-approved.tmpl" } func (m taskApproved) OnClickPath() string { return fmt.Sprintf("/profile/%s/posts?highlight=%s&update=%s", m.UserID, m.PostID, m.PostID) } type newComment struct { PostID string UserID uuid.UUID FullName string Content string } func NewComment(postId string, userId uuid.UUID, fullName string, content string) newComment { // Truncate content to 30 characters if needed truncatedContent := content if len(content) > 30 { truncatedContent = content[:30] + "..." } return newComment{ PostID: postId, UserID: userId, FullName: fullName, Content: truncatedContent, } } func (m newComment) TemplatePath() string { return "templates/web/new-comment.tmpl" } func (m newComment) OnClickPath() string { return fmt.Sprintf("/profile/%s/posts?highlight=%s", m.UserID, m.PostID) } type newCommentOrgPost struct { PostID string OrgID string FullName string Content string } func NewCommentOrgPost(postId string, orgId string, fullName string, content string) newCommentOrgPost { // Truncate content to 30 characters if needed truncatedContent := content if len(content) > 30 { truncatedContent = content[:30] + "..." } return newCommentOrgPost{ PostID: postId, OrgID: orgId, FullName: fullName, Content: truncatedContent, } } func (m newCommentOrgPost) TemplatePath() string { return "templates/web/new-comment-org-post.tmpl" } func (m newCommentOrgPost) OnClickPath() string { return fmt.Sprintf("/organizations/%s/posts?highlight=%s", m.OrgID, m.PostID) } type newReply struct { PostID string OrgID string UserID uuid.UUID FullName string Content string } func NewReply(postId string, poster uuid.UUID, fullName string, content string) newReply { // Truncate content to 30 characters if needed truncatedContent := content if len(content) > 30 { truncatedContent = content[:30] + "..." } return newReply{ PostID: postId, UserID: poster, FullName: fullName, Content: truncatedContent, } } func (m newReply) TemplatePath() string { return "templates/web/new-reply.tmpl" } func (m newReply) OnClickPath() string { if m.OrgID != "" { return fmt.Sprintf("/organizations/%s/posts?highlight=%s", m.OrgID, m.PostID) } return fmt.Sprintf("/profile/%s/posts?highlight=%s", m.UserID, m.PostID) } type vounteerHoursApprovedEmail struct { RecipientName string Organization string ApprovedHours float64 PostUpdateURL string } func NewVolunteerHoursApprovedEmail(user database.User, organization database.Organization, approvedHours float64, postId string) vounteerHoursApprovedEmail { return vounteerHoursApprovedEmail{ RecipientName: user.GivenName, Organization: organization.Name, ApprovedHours: approvedHours, PostUpdateURL: fmt.Sprintf("%s/profile/%s/posts?highlight=%s&update=%s", environment.FrontEndURL, user.UUID, postId, postId), } } func (m vounteerHoursApprovedEmail) TemplatePath() string { return "templates/email/volunteer-hours-approved.tmpl" } func (m vounteerHoursApprovedEmail) OnClickPath() string { return "" } func NewOrganizationClaimRequestConfirmationEmail(organization database.Organization, claimRequest database.OrganizationClaimRequest) organizationClaimRequestEmail { return organizationClaimRequestEmail{ OrganizationName: organization.Name, OrganizationURL: fmt.Sprintf("%s/organizations/%s", environment.FrontEndURL, organization.Slug), ClaimRequest: claimRequest, } } type organizationClaimRequestEmail struct { OrganizationName string OrganizationURL string ClaimRequest database.OrganizationClaimRequest } func (m organizationClaimRequestEmail) TemplatePath() string { return "templates/email/organization-claim-request-confirmation.tmpl" } func (m organizationClaimRequestEmail) OnClickPath() string { return "" }