feat(contact): wire contact form to real backend — sends email to contact@xpeditis.com
This commit is contained in:
parent
ed0f43ba32
commit
74221d576e
@ -8,6 +8,8 @@ import {
|
||||
Get,
|
||||
Inject,
|
||||
NotFoundException,
|
||||
InternalServerErrorException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
@ -18,7 +20,9 @@ import {
|
||||
RefreshTokenDto,
|
||||
ForgotPasswordDto,
|
||||
ResetPasswordDto,
|
||||
ContactFormDto,
|
||||
} from '../dto/auth-login.dto';
|
||||
import { EmailPort, EMAIL_PORT } from '@domain/ports/out/email.port';
|
||||
import { Public } from '../decorators/public.decorator';
|
||||
import { CurrentUser, UserPayload } from '../decorators/current-user.decorator';
|
||||
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
|
||||
@ -39,10 +43,13 @@ import { InvitationService } from '../services/invitation.service';
|
||||
@ApiTags('Authentication')
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
private readonly logger = new Logger(AuthController.name);
|
||||
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
@Inject(USER_REPOSITORY) private readonly userRepository: UserRepository,
|
||||
private readonly invitationService: InvitationService
|
||||
private readonly invitationService: InvitationService,
|
||||
@Inject(EMAIL_PORT) private readonly emailService: EmailPort
|
||||
) {}
|
||||
|
||||
/**
|
||||
@ -216,6 +223,78 @@ export class AuthController {
|
||||
return { message: 'Logout successful' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact form — forwards message to contact@xpeditis.com
|
||||
*/
|
||||
@Public()
|
||||
@Post('contact')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@ApiOperation({
|
||||
summary: 'Contact form',
|
||||
description: 'Send a contact message to the Xpeditis team.',
|
||||
})
|
||||
@ApiResponse({ status: 200, description: 'Message sent successfully' })
|
||||
async contact(@Body() dto: ContactFormDto): Promise<{ message: string }> {
|
||||
const subjectLabels: Record<string, string> = {
|
||||
demo: 'Demande de démonstration',
|
||||
pricing: 'Questions sur les tarifs',
|
||||
partnership: 'Partenariat',
|
||||
support: 'Support technique',
|
||||
press: 'Relations presse',
|
||||
careers: 'Recrutement',
|
||||
other: 'Autre',
|
||||
};
|
||||
|
||||
const subjectLabel = subjectLabels[dto.subject] || dto.subject;
|
||||
|
||||
const html = `
|
||||
<div style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
|
||||
<div style="background: #10183A; padding: 24px; border-radius: 8px 8px 0 0;">
|
||||
<h1 style="color: #34CCCD; margin: 0; font-size: 20px;">Nouveau message de contact</h1>
|
||||
</div>
|
||||
<div style="background: #f9f9f9; padding: 24px; border: 1px solid #e0e0e0;">
|
||||
<table style="width: 100%; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: #666; width: 130px; font-size: 14px;">Nom</td>
|
||||
<td style="padding: 8px 0; color: #222; font-weight: bold; font-size: 14px;">${dto.firstName} ${dto.lastName}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: #666; font-size: 14px;">Email</td>
|
||||
<td style="padding: 8px 0; font-size: 14px;"><a href="mailto:${dto.email}" style="color: #34CCCD;">${dto.email}</a></td>
|
||||
</tr>
|
||||
${dto.company ? `<tr><td style="padding: 8px 0; color: #666; font-size: 14px;">Entreprise</td><td style="padding: 8px 0; color: #222; font-size: 14px;">${dto.company}</td></tr>` : ''}
|
||||
${dto.phone ? `<tr><td style="padding: 8px 0; color: #666; font-size: 14px;">Téléphone</td><td style="padding: 8px 0; color: #222; font-size: 14px;">${dto.phone}</td></tr>` : ''}
|
||||
<tr>
|
||||
<td style="padding: 8px 0; color: #666; font-size: 14px;">Sujet</td>
|
||||
<td style="padding: 8px 0; color: #222; font-size: 14px;">${subjectLabel}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div style="margin-top: 16px; padding-top: 16px; border-top: 1px solid #ddd;">
|
||||
<p style="color: #666; font-size: 14px; margin: 0 0 8px 0;">Message :</p>
|
||||
<p style="color: #222; font-size: 14px; white-space: pre-wrap; margin: 0;">${dto.message}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div style="background: #f0f0f0; padding: 12px 24px; border-radius: 0 0 8px 8px; text-align: center;">
|
||||
<p style="color: #999; font-size: 12px; margin: 0;">Xpeditis — Formulaire de contact</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
try {
|
||||
await this.emailService.send({
|
||||
to: 'contact@xpeditis.com',
|
||||
replyTo: dto.email,
|
||||
subject: `[Contact] ${subjectLabel} — ${dto.firstName} ${dto.lastName}`,
|
||||
html,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to send contact email: ${error}`);
|
||||
throw new InternalServerErrorException("Erreur lors de l'envoi du message. Veuillez réessayer.");
|
||||
}
|
||||
|
||||
return { message: 'Message envoyé avec succès.' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgot password — sends reset email
|
||||
*/
|
||||
|
||||
@ -37,6 +37,42 @@ export class LoginDto {
|
||||
rememberMe?: boolean;
|
||||
}
|
||||
|
||||
export class ContactFormDto {
|
||||
@ApiProperty({ example: 'Jean', description: 'First name' })
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
firstName: string;
|
||||
|
||||
@ApiProperty({ example: 'Dupont', description: 'Last name' })
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
lastName: string;
|
||||
|
||||
@ApiProperty({ example: 'jean@acme.com', description: 'Sender email' })
|
||||
@IsEmail({}, { message: 'Invalid email format' })
|
||||
email: string;
|
||||
|
||||
@ApiPropertyOptional({ example: 'Acme Logistics', description: 'Company name' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
company?: string;
|
||||
|
||||
@ApiPropertyOptional({ example: '+33 6 12 34 56 78', description: 'Phone number' })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
phone?: string;
|
||||
|
||||
@ApiProperty({ example: 'demo', description: 'Subject category' })
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
subject: string;
|
||||
|
||||
@ApiProperty({ example: 'Bonjour, je souhaite...', description: 'Message body' })
|
||||
@IsString()
|
||||
@MinLength(10)
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class ForgotPasswordDto {
|
||||
@ApiProperty({
|
||||
example: 'john.doe@acme.com',
|
||||
|
||||
@ -19,6 +19,7 @@ import {
|
||||
ArrowRight,
|
||||
} from 'lucide-react';
|
||||
import { LandingHeader, LandingFooter } from '@/components/layout';
|
||||
import { sendContactForm } from '@/lib/api/auth';
|
||||
|
||||
export default function ContactPage() {
|
||||
const [formData, setFormData] = useState({
|
||||
@ -51,11 +52,22 @@ export default function ContactPage() {
|
||||
setError('');
|
||||
setIsSubmitting(true);
|
||||
|
||||
// Simulate form submission
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
|
||||
setIsSubmitting(false);
|
||||
setIsSubmitted(true);
|
||||
try {
|
||||
await sendContactForm({
|
||||
firstName: formData.firstName,
|
||||
lastName: formData.lastName,
|
||||
email: formData.email,
|
||||
company: formData.company || undefined,
|
||||
phone: formData.phone || undefined,
|
||||
subject: formData.subject,
|
||||
message: formData.message,
|
||||
});
|
||||
setIsSubmitted(true);
|
||||
} catch (err: any) {
|
||||
setError(err.message || "Une erreur est survenue lors de l'envoi. Veuillez réessayer.");
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (
|
||||
|
||||
@ -71,6 +71,22 @@ export async function getCurrentUser(): Promise<UserPayload> {
|
||||
return get<UserPayload>('/api/v1/auth/me');
|
||||
}
|
||||
|
||||
/**
|
||||
* Contact form — send message to contact@xpeditis.com
|
||||
* POST /api/v1/auth/contact
|
||||
*/
|
||||
export async function sendContactForm(data: {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
company?: string;
|
||||
phone?: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
}): Promise<{ message: string }> {
|
||||
return post<{ message: string }>('/api/v1/auth/contact', data, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forgot password — request reset email
|
||||
* POST /api/v1/auth/forgot-password
|
||||
|
||||
@ -24,8 +24,8 @@ export {
|
||||
ApiError,
|
||||
} from './client';
|
||||
|
||||
// Authentication (7 endpoints)
|
||||
export { register, login, refreshToken, logout, getCurrentUser, forgotPassword, resetPassword } from './auth';
|
||||
// Authentication (8 endpoints)
|
||||
export { register, login, refreshToken, logout, getCurrentUser, forgotPassword, resetPassword, sendContactForm } from './auth';
|
||||
|
||||
// Rates (4 endpoints)
|
||||
export { searchRates, searchCsvRates, getAvailableCompanies, getFilterOptions } from './rates';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user