Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,32 @@ AWS_SECRET_ACCESS_KEY=
AWS_STORAGE_BUCKET_NAME=bucket_name
AWS_REGION_NAME=ap-southeast-2

# ======================
# Google Calendar
# ======================
GOOGLE_CREDENTIALS_FILE=api/booking/google_calendar/google_calendar_service.json
GOOGLE_CALENDAR_ID=

# ======================
# Frontend URL
# ======================
FRONTEND_URL="http://localhost:3000"

# ======================
# Email configuration
# ======================
EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587

# Must be replaced with your real Gmail address
EMAIL_HOST_USER=your_email@example.com

# IMPORTANT:
# Gmail requires an App Password (not your normal password)
# Learn more: https://support.google.com/accounts/answer/185833
EMAIL_HOST_PASSWORD=your_app_password_here

EMAIL_USE_TLS=True
EMAIL_USE_SSL=False

FRONTEND_URL="http://localhost:3000"
123 changes: 123 additions & 0 deletions server/api/email_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from typing import Iterable

from django.conf import settings
from django.core.mail import send_mail
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.templatetags.static import static


# Template paths under api/templates/emails
BOOKING_CONFIRMED_TEMPLATE = "emails/booking_confirmed.html"
BOOKING_CANCELLED_TEMPLATE = "emails/booking_cancelled.html"


def get_bloom_logo_url() -> str:
"""
URL for the Bloom logo used in emails.

Uses BLOOM_LOGO_URL from settings if present, otherwise falls
back to the static path (useful for local/dev).
"""
logo_url = getattr(settings, "BLOOM_LOGO_URL", None)
if logo_url:
return logo_url

# Fallback – relative static path; fine for local / console email previews
return static("images/bloom_logo.png")


def send_simple_email(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we'll have html content for the email message. I just added the design in figma:
image
image

pls update according to this

subject: str,
message: str | None,
recipients: Iterable[str],
*,
html_template: str | None = None,
context: dict | None = None,
from_email: str | None = None,
fail_silently: bool = False,
) -> int:
"""
Wrapper for Django's send_mail using EMAIL_HOST_USER.

- If `html_template` is provided, it renders that template with `context`
and sends it as HTML email.
- If `message` is None, it will be generated by stripping HTML tags from
the rendered HTML so a plain-text body is still sent.
- Existing plain-text usage still works: pass `subject`, `message`,
and `recipients` without `html_template`.
"""
if from_email is None:
from_email = settings.EMAIL_HOST_USER

html_message = None

if html_template is not None:
context = context or {}
html_message = render_to_string(html_template, context)
# Derive plain text from HTML if not explicitly provided
if message is None:
message = strip_tags(html_message)

# Django requires a non-empty string for `message`
if message is None:
message = ""

return send_mail(
subject=subject,
message=message,
from_email=from_email,
recipient_list=list(recipients),
fail_silently=fail_silently,
html_message=html_message,
)


def send_booking_confirmed_email(
recipients: Iterable[str],
*,
context: dict,
subject: str = "Booking confirmed!",
fail_silently: bool = False,
) -> int:
"""
Convenience wrapper for the booking confirmed HTML template.
Expects `context` to match the variables used in
emails/booking_confirmed.html.
"""
ctx = dict(context or {})
ctx.setdefault("bloom_logo_url", get_bloom_logo_url())

return send_simple_email(
subject=subject,
message=None,
recipients=recipients,
html_template=BOOKING_CONFIRMED_TEMPLATE,
context=ctx,
fail_silently=fail_silently,
)


def send_booking_cancelled_email(
recipients: Iterable[str],
*,
context: dict,
subject: str = "Booking cancelled!",
fail_silently: bool = False,
) -> int:
"""
Convenience wrapper for the booking cancelled HTML template.
Expects `context` to match the variables used in
emails/booking_cancelled.html.
"""
ctx = dict(context or {})
ctx.setdefault("bloom_logo_url", get_bloom_logo_url())

return send_simple_email(
subject=subject,
message=None,
recipients=recipients,
html_template=BOOKING_CANCELLED_TEMPLATE,
context=ctx,
fail_silently=fail_silently,
)
41 changes: 28 additions & 13 deletions server/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"DIRS": [BASE_DIR / "api" / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
Expand Down Expand Up @@ -180,6 +180,32 @@
"DEFAULT_AUTHENTICATION_CLASSES": ["rest_framework_simplejwt.authentication.JWTAuthentication"],
}

# =========================
# Email configuration
# =========================

EMAIL_BACKEND = os.environ.get(
"EMAIL_BACKEND",
"django.core.mail.backends.smtp.EmailBackend",
)

EMAIL_HOST = os.environ.get("EMAIL_HOST", "smtp.gmail.com")
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", "587"))
EMAIL_HOST_USER = os.environ.get("EMAIL_HOST_USER", "")
EMAIL_HOST_PASSWORD = os.environ.get("EMAIL_HOST_PASSWORD", "")

# TLS / SSL flags come from env as strings ("True"/"False")
EMAIL_USE_TLS = os.environ.get("EMAIL_USE_TLS", "True") == "True"
EMAIL_USE_SSL = os.environ.get("EMAIL_USE_SSL", "False") == "True"

DEFAULT_FROM_EMAIL = os.environ.get("EMAIL_HOST_USER", "")
SERVER_EMAIL = os.environ.get("SERVER_EMAIL", DEFAULT_FROM_EMAIL)


# =========================
# AWS S3 storage configuration
# =========================

AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID")
AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY")
AWS_STORAGE_BUCKET_NAME = os.getenv("AWS_STORAGE_BUCKET_NAME")
Expand All @@ -195,18 +221,7 @@
])

# By default, models using ImageField/FileField will use S3 for storage, as configured in STORAGES.
# example model:
# ```
# class Room(models.Model):
# name = models.CharField(max_length=255)
# image = models.ImageField(upload_to="rooms/")
# ```
# Only specify a custom storage backend if you need to use something other than S3.
# Example: to check the default storage backend being used:
# ```
# from django.core.files.storage import default_storage
# print(default_storage.__class__)
# ```
if USE_S3:
STORAGES = {
"default": {
Expand Down Expand Up @@ -239,4 +254,4 @@
}
MEDIA_URL = "/media/"

AUTH_USER_MODEL = 'api_user.CustomUser'
AUTH_USER_MODEL = "api_user.CustomUser"
Binary file added server/api/static/images/bloom_logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
118 changes: 118 additions & 0 deletions server/api/templates/emails/base_booking_email.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>{% block title %}Bloom Notification{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
body {
margin: 0;
padding: 0;
background-color: #f4f4f6;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

.wrapper {
width: 100%;
padding: 32px 0;
}

.container {
max-width: 640px;
margin: 0 auto;
background-color: #ffffff;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.08);
}

/* ==== HEADER (Figma-style) ==== */
.header {
background-color: #000000; /* black bar */
padding: 24px 0;
text-align: center;
}

.logo-row {
display: inline-block;
}

.logo-image {
display: inline-block;
background-color: #ffffff; /* white box behind logo */
padding: 8px 24px; /* spacing around logo */
border-radius: 4px; /* optional: match card style */
}

.logo-image img {
display: block;
height: 40px; /* consistent logo size */
width: auto;
}

/* ==== MAIN CONTENT ==== */
.content {
padding: 32px 32px 40px;
text-align: center;
}

.button {
display: inline-block;
padding: 12px 32px;
border-radius: 4px;
text-decoration: none;
font-size: 14px;
font-weight: 600;
background-color: #38bdf8;
color: #ffffff !important;
}

.footer {
margin-top: 24px;
font-size: 11px;
color: #9ca3af;
line-height: 1.6;
}

@media (max-width: 640px) {
.container {
border-radius: 0;
}
.content {
padding: 24px 16px 32px;
}
}

{% block extra_styles %}{% endblock %}
</style>
</head>

<body>
<div class="wrapper">
<div class="container">

<!-- HEADER -->
<div class="header">
<div class="logo-row">
<div class="logo-image">
<img src="{{ bloom_logo_url }}" alt="Bloom logo" />
</div>
</div>
</div>

<!-- MAIN CONTENT -->
<div class="content">
{% block content %}{% endblock %}

<div class="footer">
{% block footer %}
You’re receiving this email because you made or updated a booking
with Bloom Room Booker. Please do not reply to this automated message.
{% endblock %}
</div>
</div>

</div>
</div>
</body>
</html>
33 changes: 33 additions & 0 deletions server/api/templates/emails/booking_cancelled.html
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please don't hard code, think about using a function and pass the details. pls use the bloom logo that you can find in figma thanks!

Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{% extends "emails/base_booking_email.html" %}

{% block title %}Booking cancelled{% endblock %}

{% block extra_styles %}
h1 {
margin: 0 0 12px;
font-size: 22px;
font-weight: 600;
color: #111827;
}

.subtitle {
margin: 0 0 28px;
font-size: 14px;
color: #4b5563;
line-height: 1.6;
}
{% endblock %}

{% block content %}
<h1>Booking cancelled!</h1>

<p class="subtitle">
Your {{ room_name }} booking for
{{ start|date:"d M Y" }} from {{ start|time:"g:i A" }}
to {{ end|time:"g:i A" }} has been cancelled.
</p>

{% if book_room_url %}
<a href="{{ book_room_url }}" class="button">Book room</a>
{% endif %}
{% endblock %}
Loading