perfect but the email template has to have an attached pdf where the es
This commit is contained in:
Generated
+43
@@ -45,6 +45,7 @@
|
||||
"next": "15.3.3",
|
||||
"nodemailer": "^6.9.14",
|
||||
"patch-package": "^8.0.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -2671,6 +2672,24 @@
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/standard-fonts": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
|
||||
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@pdf-lib/upng": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
|
||||
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pako": "^1.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@pkgjs/parseargs": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -8058,6 +8077,12 @@
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="
|
||||
},
|
||||
"node_modules/pako": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -8176,6 +8201,24 @@
|
||||
"through": "~2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-lib": {
|
||||
"version": "1.17.1",
|
||||
"resolved": "https://registry.npmjs.org/pdf-lib/-/pdf-lib-1.17.1.tgz",
|
||||
"integrity": "sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pdf-lib/standard-fonts": "^1.0.0",
|
||||
"@pdf-lib/upng": "^1.0.1",
|
||||
"pako": "^1.0.11",
|
||||
"tslib": "^1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/pdf-lib/node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/pend": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"next": "15.3.3",
|
||||
"nodemailer": "^6.9.14",
|
||||
"patch-package": "^8.0.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"react": "^18.3.1",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "^18.3.1",
|
||||
|
||||
@@ -1,22 +1,71 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
async function getSmtpSettings(): Promise<Record<string, string>> {
|
||||
const response = await fetch('/api/settings/smtp');
|
||||
if (!response.ok) throw new Error('Failed to fetch SMTP settings');
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async function updateSmtpSettings(settings: Record<string, string>): Promise<{ success: boolean, message: string }> {
|
||||
const response = await fetch('/api/settings/smtp', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(settings),
|
||||
});
|
||||
const data = await response.json();
|
||||
return { success: response.ok, message: data.message };
|
||||
}
|
||||
|
||||
export default function SmtpSettingsPage() {
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleSaveChanges = () => {
|
||||
// In a real app, you would save these settings.
|
||||
// For now, we'll just show a success toast.
|
||||
toast({
|
||||
title: 'Success!',
|
||||
description: 'SMTP settings saved (simulation).',
|
||||
const [settings, setSettings] = useState({
|
||||
'smtp_server': '',
|
||||
'smtp_port': '',
|
||||
'smtp_username': '',
|
||||
'smtp_password': ''
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function loadSettings() {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const fetchedSettings = await getSmtpSettings();
|
||||
setSettings(s => ({...s, ...fetchedSettings}));
|
||||
} catch (error) {
|
||||
toast({ variant: 'destructive', title: 'Error', description: 'Could not load SMTP settings.' });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}
|
||||
loadSettings();
|
||||
}, [toast]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { id, value } = e.target;
|
||||
setSettings(prev => ({ ...prev, [id]: value }));
|
||||
};
|
||||
|
||||
const handleSaveChanges = async () => {
|
||||
try {
|
||||
const result = await updateSmtpSettings(settings);
|
||||
if (result.success) {
|
||||
toast({ title: 'Success!', description: result.message });
|
||||
} else {
|
||||
throw new Error(result.message);
|
||||
}
|
||||
} catch (error: any) {
|
||||
toast({ variant: 'destructive', title: 'Error', description: error.message || "Failed to save settings." });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -35,27 +84,40 @@ export default function SmtpSettingsPage() {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{isLoading ? (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Skeleton className="h-10 w-full" />
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
<Skeleton className="h-10 w-full" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="smtp-server">SMTP Server</Label>
|
||||
<Input id="smtp-server" placeholder="smtp.example.com" />
|
||||
<Label htmlFor="smtp_server">SMTP Server</Label>
|
||||
<Input id="smtp_server" placeholder="smtp.example.com" value={settings.smtp_server} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="smtp-port">Port</Label>
|
||||
<Input id="smtp-port" placeholder="587" />
|
||||
<Label htmlFor="smtp_port">Port</Label>
|
||||
<Input id="smtp_port" placeholder="587" value={settings.smtp_port} onChange={handleChange} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="smtp-username">Username</Label>
|
||||
<Input id="smtp-username" placeholder="your_username" />
|
||||
<Label htmlFor="smtp_username">Username</Label>
|
||||
<Input id="smtp_username" placeholder="your_username" value={settings.smtp_username} onChange={handleChange} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="smtp-password">Password</Label>
|
||||
<Input id="smtp-password" type="password" placeholder="••••••••" />
|
||||
<Label htmlFor="smtp_password">Password</Label>
|
||||
<Input id="smtp_password" type="password" placeholder="••••••••" value={settings.smtp_password} onChange={handleChange} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Button onClick={handleSaveChanges}>Save Changes</Button>
|
||||
<Button onClick={handleSaveChanges} disabled={isLoading}>Save Changes</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import db from '@/lib/db';
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const stmt = db.prepare("SELECT key, value FROM settings WHERE key LIKE 'smtp_%'");
|
||||
const settings = stmt.all() as { key: string, value: string }[];
|
||||
const result = settings.reduce((acc, setting) => {
|
||||
acc[setting.key] = setting.value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error('Failed to get SMTP settings:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
try {
|
||||
const settings = await req.json();
|
||||
|
||||
const requiredKeys = ['smtp_server', 'smtp_port', 'smtp_username', 'smtp_password'];
|
||||
for(const key of requiredKeys) {
|
||||
if(typeof settings[key] === 'undefined') {
|
||||
return NextResponse.json({ message: `Missing required setting: ${key}` }, { status: 400 });
|
||||
}
|
||||
}
|
||||
|
||||
const stmt = db.prepare('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)');
|
||||
const transaction = db.transaction((settingsToSave) => {
|
||||
for (const key in settingsToSave) {
|
||||
stmt.run(key, settingsToSave[key]);
|
||||
}
|
||||
});
|
||||
|
||||
transaction(settings);
|
||||
|
||||
return NextResponse.json({ message: 'SMTP settings updated successfully' });
|
||||
} catch (error) {
|
||||
console.error('Failed to update SMTP settings:', error);
|
||||
return NextResponse.json({ message: 'Internal Server Error' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -267,6 +267,7 @@ export function CostEstimatorForm() {
|
||||
onReset={handleReset}
|
||||
customHours={customHours}
|
||||
readyMadeHours={readyMadeHours}
|
||||
formData={formData}
|
||||
/>
|
||||
)
|
||||
case 12:
|
||||
@@ -291,7 +292,7 @@ export function CostEstimatorForm() {
|
||||
const isResultsStep = currentStep === 11;
|
||||
|
||||
return (
|
||||
<Card className={`mt-8 w-full max-w-4xl shadow-2xl overflow-hidden ${isResultsStep ? 'bg-transparent border-none' : ''}`}>
|
||||
<Card className={`mt-8 w-full max-w-4xl shadow-2xl overflow-hidden ${isResultsStep ? 'bg-transparent border-none shadow-none' : ''}`}>
|
||||
<CardContent className={`p-4 sm:p-8 ${isResultsStep ? 'p-0 sm:p-0' : ''}`}>
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
|
||||
@@ -4,14 +4,28 @@
|
||||
import type { FormData } from './cost-estimator-form';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Info, Mail, RefreshCw } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { Info, Mail } from 'lucide-react';
|
||||
import React, { useState, useTransition } from 'react';
|
||||
import { Tooltip, TooltipProvider, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { sendEstimateEmail } from '@/lib/actions/send-estimate';
|
||||
|
||||
type Step11Props = {
|
||||
onReset: () => void;
|
||||
customHours: number;
|
||||
readyMadeHours: number;
|
||||
formData: FormData;
|
||||
};
|
||||
|
||||
const designHoursMap = { custom: 60, mockups: 30, existing: 0 };
|
||||
@@ -99,7 +113,50 @@ export const calculateTotalHours = (formData: FormData) => {
|
||||
return { customHours, readyMadeHours };
|
||||
};
|
||||
|
||||
export function Step11Results({ onReset, customHours, readyMadeHours }: Step11Props) {
|
||||
export function Step11Results({ onReset, customHours, readyMadeHours, formData }: Step11Props) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const { toast } = useToast();
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const form = new FormData(event.currentTarget);
|
||||
const name = form.get('name') as string;
|
||||
const email = form.get('email') as string;
|
||||
|
||||
if (!name || !email) {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Missing Information",
|
||||
description: "Please enter both your name and email.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
const result = await sendEstimateEmail({
|
||||
name,
|
||||
email,
|
||||
customHours,
|
||||
readyMadeHours,
|
||||
formData,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
toast({
|
||||
title: "Estimate Sent!",
|
||||
description: "Check your email for the detailed estimate PDF.",
|
||||
});
|
||||
setDialogOpen(false);
|
||||
} else {
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: "Error Sending Email",
|
||||
description: result.message || "An unexpected error occurred.",
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center text-center">
|
||||
@@ -151,10 +208,43 @@ export function Step11Results({ onReset, customHours, readyMadeHours }: Step11Pr
|
||||
<p className="mt-6 text-sm text-muted-foreground">If you need a detailed estimate for your project, we can send it to your email.</p>
|
||||
|
||||
<div className="mt-8 flex items-center gap-4">
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="lg" variant="outline">
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
Send Estimate on Email
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Send Estimate</DialogTitle>
|
||||
<DialogDescription>
|
||||
Enter your name and email to receive the estimate PDF.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Name
|
||||
</Label>
|
||||
<Input id="name" name="name" className="col-span-3" required />
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="email" className="text-right">
|
||||
Email
|
||||
</Label>
|
||||
<Input id="email" name="email" type="email" className="col-span-3" required />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={isPending}>
|
||||
{isPending ? "Sending..." : "Send to Email"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button onClick={onReset} size="lg" variant="link" className="text-muted-foreground">
|
||||
Start over
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
|
||||
'use server';
|
||||
|
||||
import { z } from 'zod';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { PDFDocument, rgb, StandardFonts } from 'pdf-lib';
|
||||
import db from '@/lib/db';
|
||||
import { getEmailTemplate } from './email';
|
||||
import type { FormData } from '@/components/cost-estimator/cost-estimator-form';
|
||||
|
||||
const inputSchema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().email(),
|
||||
customHours: z.number(),
|
||||
readyMadeHours: z.number(),
|
||||
formData: z.any(), // Keeping this flexible for now
|
||||
});
|
||||
|
||||
type InputType = z.infer<typeof inputSchema>;
|
||||
|
||||
// Helper to get SMTP settings from the database
|
||||
async function getSmtpSettings() {
|
||||
const stmt = db.prepare("SELECT key, value FROM settings WHERE key LIKE 'smtp_%'");
|
||||
const settings = stmt.all() as { key: string, value: string }[];
|
||||
const config = settings.reduce((acc, setting) => {
|
||||
acc[setting.key.replace('smtp_', '')] = setting.value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
return {
|
||||
host: config.server,
|
||||
port: parseInt(config.port || '587', 10),
|
||||
secure: parseInt(config.port || '587', 10) === 465, // true for 465, false for other ports
|
||||
auth: {
|
||||
user: config.username,
|
||||
pass: config.password,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
async function createEstimatePdf(data: InputType): Promise<Buffer> {
|
||||
const { name, customHours, readyMadeHours, formData } = data;
|
||||
const pdfDoc = await PDFDocument.create();
|
||||
const page = pdfDoc.addPage();
|
||||
const { width, height } = page.getSize();
|
||||
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
||||
const boldFont = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
||||
const brandColor = rgb(0.16, 0.47, 0.94); // #2979FF
|
||||
|
||||
let y = height - 50;
|
||||
|
||||
// Header
|
||||
page.drawText('EstimateFlow', { x: 50, y, font: boldFont, size: 24, color: brandColor });
|
||||
y -= 20;
|
||||
page.drawLine({
|
||||
start: { x: 50, y: y },
|
||||
end: { x: width - 50, y: y },
|
||||
thickness: 2,
|
||||
color: brandColor,
|
||||
});
|
||||
y -= 30;
|
||||
|
||||
// Subheader
|
||||
page.drawText(`Project Estimate for: ${name}`, { x: 50, y, font: boldFont, size: 18 });
|
||||
y -= 20;
|
||||
page.drawText(`Date: ${new Date().toLocaleDateString()}`, { x: 50, y, font, size: 12, color: rgb(0.3, 0.3, 0.3) });
|
||||
y -= 40;
|
||||
|
||||
// Summary
|
||||
page.drawText('Estimate Summary', { x: 50, y, font: boldFont, size: 16 });
|
||||
y -= 25;
|
||||
|
||||
const drawEstimateBox = (title: string, hours: number, description: string, boxY: number) => {
|
||||
page.drawRectangle({
|
||||
x: 50,
|
||||
y: boxY - 80,
|
||||
width: width - 100,
|
||||
height: 90,
|
||||
borderColor: rgb(0.9, 0.9, 0.9),
|
||||
borderWidth: 1,
|
||||
color: rgb(0.98, 0.98, 0.98),
|
||||
borderRadius: 5
|
||||
});
|
||||
page.drawText(title, { x: 65, y: boxY, font: boldFont, size: 14 });
|
||||
page.drawText(`${hours}+ hours`, { x: 65, y: boxY - 30, font: boldFont, size: 28, color: brandColor });
|
||||
page.drawText(description, { x: 65, y: boxY - 60, font, size: 10, color: rgb(0.4, 0.4, 0.4) });
|
||||
}
|
||||
|
||||
drawEstimateBox('Custom Development', customHours, 'A fully custom solution tailored to your specific needs.', y);
|
||||
y -= 110;
|
||||
drawEstimateBox('Ready-Made Tools', readyMadeHours, 'Leveraging pre-built components for a faster turnaround.', y);
|
||||
y -= 110;
|
||||
|
||||
// Disclaimer
|
||||
page.drawText('Disclaimer:', { x: 50, y, font: boldFont, size: 12 });
|
||||
y -= 20;
|
||||
const disclaimer = 'This is a preliminary estimate based on the provided selections. The final cost may vary after a detailed project analysis and scope definition. Please contact us for a comprehensive quote.';
|
||||
page.drawText(disclaimer, {
|
||||
x: 50,
|
||||
y,
|
||||
font,
|
||||
size: 10,
|
||||
color: rgb(0.5, 0.5, 0.5),
|
||||
lineHeight: 14,
|
||||
maxWidth: width - 100
|
||||
});
|
||||
|
||||
const pdfBytes = await pdfDoc.save();
|
||||
return Buffer.from(pdfBytes);
|
||||
}
|
||||
|
||||
|
||||
export async function sendEstimateEmail(data: InputType): Promise<{ success: boolean, message?: string }> {
|
||||
const validation = inputSchema.safeParse(data);
|
||||
if (!validation.success) {
|
||||
return { success: false, message: 'Invalid data provided.' };
|
||||
}
|
||||
|
||||
try {
|
||||
const smtpSettings = await getSmtpSettings();
|
||||
if (!smtpSettings.host || !smtpSettings.auth.user || !smtpSettings.auth.pass) {
|
||||
return { success: false, message: 'SMTP settings are not configured. Please configure them in the admin panel.' };
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport(smtpSettings);
|
||||
|
||||
await transporter.verify();
|
||||
|
||||
const pdfBuffer = await createEstimatePdf(validation.data);
|
||||
const emailTemplate = await getEmailTemplate();
|
||||
|
||||
const mailOptions = {
|
||||
from: `EstimateFlow <${smtpSettings.auth.user}>`,
|
||||
to: validation.data.email,
|
||||
subject: emailTemplate.subject.replace('[User Name]', validation.data.name),
|
||||
html: emailTemplate.body.replace('[User Name]', validation.data.name).replace('[EstimateDetails]', `
|
||||
<p style="font-size: 16px; line-height: 1.6;">
|
||||
Please find your detailed project estimate attached to this email as a PDF.
|
||||
</p>
|
||||
`),
|
||||
attachments: [
|
||||
{
|
||||
filename: 'EstimateFlow_Project_Estimate.pdf',
|
||||
content: pdfBuffer,
|
||||
contentType: 'application/pdf',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
return { success: true };
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('Failed to send estimate email:', error);
|
||||
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
return { success: false, message: 'Could not connect to SMTP server. Check server address and port.' };
|
||||
}
|
||||
if (error.code === 'EAUTH') {
|
||||
return { success: false, message: 'SMTP authentication failed. Check username and password.' };
|
||||
}
|
||||
|
||||
return { success: false, message: error.message || 'An unexpected error occurred while sending the email.' };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
|
||||
export type IllustrationSelection = {
|
||||
has2d: boolean;
|
||||
is2dAnimated: 'static' | 'animated' | null;
|
||||
has3d: boolean;
|
||||
is3dAnimated: 'static' | 'animated' | null;
|
||||
};
|
||||
|
||||
export type BrandingSelection = 'full-cycle' | 'brush-up' | 'logo-only' | 'none' | null;
|
||||
|
||||
export type ShoppingCartSelection = {
|
||||
hasCart: boolean | null;
|
||||
multiplePurchases: boolean | null;
|
||||
}
|
||||
|
||||
export type FormData = {
|
||||
projectType: 'website' | 'mobile-app' | 'platform' | null;
|
||||
serviceType: 'entire-project' | 'development' | 'ui-ux-design' | 'identity-branding' | null;
|
||||
projectStage: number;
|
||||
designProcess: 'custom' | 'mockups' | 'existing' | null;
|
||||
pageCount: number;
|
||||
animatedElements: boolean | null;
|
||||
illustrations: IllustrationSelection;
|
||||
branding: BrandingSelection;
|
||||
additionalFeatures: string[];
|
||||
shoppingCart: ShoppingCartSelection;
|
||||
uiUxDesignProcess: 'custom' | 'concepts' | null;
|
||||
};
|
||||
Reference in New Issue
Block a user