From 0e1eb056a435e83761f90d0a981b2c2e11187766 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 00:46:32 -0600 Subject: [PATCH] contact form with email --- .../backend/controller/ContactController.java | 40 +++++++++++ .../petshop/backend/service/EmailService.java | 13 ++++ web/app/contact/page.js | 67 +++++++++++++++++++ web/app/globals.css | 10 +++ 4 files changed, 130 insertions(+) create mode 100644 backend/src/main/java/com/petshop/backend/controller/ContactController.java diff --git a/backend/src/main/java/com/petshop/backend/controller/ContactController.java b/backend/src/main/java/com/petshop/backend/controller/ContactController.java new file mode 100644 index 00000000..c6b4cf1d --- /dev/null +++ b/backend/src/main/java/com/petshop/backend/controller/ContactController.java @@ -0,0 +1,40 @@ +package com.petshop.backend.controller; + +import com.petshop.backend.entity.User; +import com.petshop.backend.repository.UserRepository; +import com.petshop.backend.service.EmailService; +import com.petshop.backend.util.AuthenticationHelper; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/v1/contact") +public class ContactController { + + private final EmailService emailService; + private final UserRepository userRepository; + + public ContactController(EmailService emailService, UserRepository userRepository) { + this.emailService = emailService; + this.userRepository = userRepository; + } + + public record ContactRequest( + @NotBlank @Size(max = 150) String subject, + @NotBlank @Size(max = 2000) String body + ) {} + + @PostMapping + public ResponseEntity sendContactEmail(@Valid @RequestBody ContactRequest req) { + Long userId = AuthenticationHelper.getAuthenticatedUserId(); + User user = userRepository.findById(userId).orElseThrow(); + emailService.sendContactMessage(user, req.subject(), req.body()); + return ResponseEntity.ok().build(); + } +} diff --git a/backend/src/main/java/com/petshop/backend/service/EmailService.java b/backend/src/main/java/com/petshop/backend/service/EmailService.java index 667aad08..b944c053 100644 --- a/backend/src/main/java/com/petshop/backend/service/EmailService.java +++ b/backend/src/main/java/com/petshop/backend/service/EmailService.java @@ -129,6 +129,19 @@ public class EmailService { } } + public void sendContactMessage(User user, String subject, String body) { + if (user.getEmail() == null || user.getEmail().isBlank()) return; + String html = """ +
+

Contact form message

+

From: %s (%s)

+

Subject: %s

+
+

%s

+
""".formatted(esc(firstName(user)), esc(user.getEmail()), esc(subject), esc(body)); + send(user.getId(), user.getEmail(), "Contact: " + subject, html); + } + public void sendChatTranscript(Conversation conversation, List messages, User customer) { if (customer == null || customer.getEmail() == null || customer.getEmail().isBlank()) return; String subject = "Your PetShop support transcript"; diff --git a/web/app/contact/page.js b/web/app/contact/page.js index f5da828f..79f9b555 100644 --- a/web/app/contact/page.js +++ b/web/app/contact/page.js @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect } from "react"; +import { useAuth } from "@/context/AuthContext"; function getStoreImage(store) { if (store.imageUrl) return store.imageUrl; @@ -12,10 +13,17 @@ function getStoreImage(store) { } export default function ContactPage() { + const { token } = useAuth(); const [locations, setLocations] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [subject, setSubject] = useState(""); + const [body, setBody] = useState(""); + const [sending, setSending] = useState(false); + const [sendError, setSendError] = useState(null); + const [sendSuccess, setSendSuccess] = useState(false); + useEffect(() => { const params = new URLSearchParams({ page: "0", size: "100", sort: "storeName,asc" }); fetch(`/api/v1/stores?${params}`) @@ -28,6 +36,27 @@ export default function ContactPage() { .finally(() => setLoading(false)); }, []); + async function handleSend(e) { + e.preventDefault(); + setSending(true); + setSendError(null); + try { + const res = await fetch("/api/v1/contact", { + method: "POST", + headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` }, + body: JSON.stringify({ subject, body }), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + setSendSuccess(true); + setSubject(""); + setBody(""); + } catch (err) { + setSendError("Failed to send message. Please try again."); + } finally { + setSending(false); + } + } + return (
@@ -44,6 +73,44 @@ export default function ContactPage() {

Hours: Mon–Sat, 9:00 AM – 6:00 PM

+ {token && ( +
+

Send Us a Message

+ {sendSuccess ? ( +

Your message has been sent. We'll be in touch soon.

+ ) : ( +
+ +