feat: Implement contact message functionality including API endpoints, database storage, and email notifications.
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "ContactMessage" (
|
||||||
|
"id" TEXT NOT NULL,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"subject" TEXT,
|
||||||
|
"message" TEXT NOT NULL,
|
||||||
|
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
CONSTRAINT "ContactMessage_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
@@ -135,3 +135,12 @@ model File {
|
|||||||
|
|
||||||
@@index([folder, originalName])
|
@@index([folder, originalName])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ContactMessage {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
name String
|
||||||
|
email String
|
||||||
|
subject String?
|
||||||
|
message String
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { CategoryModule } from './category/category.module';
|
|||||||
import { RestaurantModule } from './restaurant/restaurant.module';
|
import { RestaurantModule } from './restaurant/restaurant.module';
|
||||||
import { ItemModule } from './item/item.module';
|
import { ItemModule } from './item/item.module';
|
||||||
import { FileModule } from './file/file.module';
|
import { FileModule } from './file/file.module';
|
||||||
|
import { ContactModule } from './contact/contact.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -17,6 +18,7 @@ import { FileModule } from './file/file.module';
|
|||||||
RestaurantModule,
|
RestaurantModule,
|
||||||
ItemModule,
|
ItemModule,
|
||||||
FileModule,
|
FileModule,
|
||||||
|
ContactModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Body, Controller, Get, Post, UseGuards } from '@nestjs/common';
|
||||||
|
import { ContactService } from './contact.service';
|
||||||
|
import { AllowAnonymous, AuthGuard } from '@thallesp/nestjs-better-auth';
|
||||||
|
import { CreateContactDto } from './dto/createContant.dto';
|
||||||
|
|
||||||
|
@Controller('contact')
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
export class ContactController {
|
||||||
|
constructor(private readonly contactService: ContactService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
@AllowAnonymous()
|
||||||
|
async create(@Body() data: CreateContactDto) {
|
||||||
|
return this.contactService.create(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
async findAll() {
|
||||||
|
return this.contactService.findAll();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ContactService } from './contact.service';
|
||||||
|
import { ContactController } from './contact.controller';
|
||||||
|
import { PrismaModule } from 'src/prisma/prisma.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [PrismaModule],
|
||||||
|
providers: [ContactService],
|
||||||
|
controllers: [ContactController],
|
||||||
|
})
|
||||||
|
export class ContactModule {}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { emailService } from '../shared/services/email.service';
|
||||||
|
import { CreateContactDto } from './dto/createContant.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ContactService {
|
||||||
|
constructor(private prisma: PrismaService) {}
|
||||||
|
|
||||||
|
async create(data: CreateContactDto) {
|
||||||
|
const message = await this.prisma.contactMessage.create({
|
||||||
|
data: {
|
||||||
|
name: data.name,
|
||||||
|
email: data.email,
|
||||||
|
subject: data.subject,
|
||||||
|
message: data.message,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send email notification (fire and forget, or await if critical)
|
||||||
|
// We'll catch errors so it doesn't fail the request if email fails
|
||||||
|
try {
|
||||||
|
await emailService.sendContactMessageNotification({
|
||||||
|
name: message.name,
|
||||||
|
email: message.email,
|
||||||
|
subject: message.subject || undefined,
|
||||||
|
message: message.message,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send contact notification email:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll() {
|
||||||
|
return this.prisma.contactMessage.findMany({
|
||||||
|
orderBy: {
|
||||||
|
createdAt: 'desc',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { IsString, IsEmail, IsNotEmpty, IsOptional, MaxLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class CreateContactDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(100)
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsEmail()
|
||||||
|
@IsNotEmpty()
|
||||||
|
email: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@MaxLength(20)
|
||||||
|
subject?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MaxLength(1000)
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
@@ -47,5 +47,8 @@ export const config = {
|
|||||||
user: process.env.EMAIL_USER,
|
user: process.env.EMAIL_USER,
|
||||||
password: process.env.EMAIL_PASSWORD,
|
password: process.env.EMAIL_PASSWORD,
|
||||||
from: process.env.EMAIL_FROM || '"Zemenu" <no-reply@zemenu.com>',
|
from: process.env.EMAIL_FROM || '"Zemenu" <no-reply@zemenu.com>',
|
||||||
|
contactNotificationEmails: process.env.CONTACT_NOTIFICATION_EMAILS
|
||||||
|
? process.env.CONTACT_NOTIFICATION_EMAILS.split(',').map(email => email.trim())
|
||||||
|
: [process.env.EMAIL_FROM || 'no-reply@zemenu.com'],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -52,6 +52,21 @@ export class EmailService {
|
|||||||
await this.sendEmail(to, 'Reset your password', html);
|
await this.sendEmail(to, 'Reset your password', html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendContactMessageNotification(data: { name: string; email: string; subject?: string; message: string }) {
|
||||||
|
const content = `
|
||||||
|
<p>You have received a new contact message from <strong>${data.name}</strong> (${data.email}).</p>
|
||||||
|
<p><strong>Subject:</strong> ${data.subject || 'No Subject'}</p>
|
||||||
|
<hr style="border: 0; border-top: 1px solid #eee; margin: 20px 0;">
|
||||||
|
<p style="white-space: pre-wrap;">${data.message}</p>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const html = this.getHtmlTemplate('New Contact Message', content);
|
||||||
|
|
||||||
|
// Send to the configured notification emails
|
||||||
|
const recipients = config.email.contactNotificationEmails;
|
||||||
|
await this.sendEmail(recipients.join(','), `New Contact Message: ${data.subject || 'No Subject'}`, html);
|
||||||
|
}
|
||||||
|
|
||||||
private getHtmlTemplate(title: string, content: string): string {
|
private getHtmlTemplate(title: string, content: string): string {
|
||||||
const logoUrl = 'https://res.cloudinary.com/dmxoohiwo/image/upload/v1764315105/logo_fpczsd.png';
|
const logoUrl = 'https://res.cloudinary.com/dmxoohiwo/image/upload/v1764315105/logo_fpczsd.png';
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|||||||
Reference in New Issue
Block a user