feat: Add contact page with Resend API integration
This commit is contained in:
parent
0390a370c2
commit
5b0d8a9ffc
|
@ -15,6 +15,7 @@
|
|||
"@astrojs/tailwind": "latest",
|
||||
"astro": "latest",
|
||||
"nodemailer": "^6.10.1",
|
||||
"resend": "^4.4.1",
|
||||
"tailwindcss": "^3.0.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -1358,6 +1359,24 @@
|
|||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-email/render": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.6.tgz",
|
||||
"integrity": "sha512-zNueW5Wn/4jNC1c5LFgXzbUdv5Lhms+FWjOvWAhal7gx5YVf0q6dPJ0dnR70+ifo59gcMLwCZEaTS9EEuUhKvQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"html-to-text": "9.0.5",
|
||||
"prettier": "3.5.3",
|
||||
"react-promise-suspense": "0.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
||||
|
@ -1646,6 +1665,19 @@
|
|||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@selderee/plugin-htmlparser2": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
|
||||
"integrity": "sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domhandler": "^5.0.3",
|
||||
"selderee": "^0.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/core": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.3.0.tgz",
|
||||
|
@ -2677,6 +2709,15 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
|
||||
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/defu": {
|
||||
"version": "6.1.4",
|
||||
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
|
||||
|
@ -2766,6 +2807,73 @@
|
|||
"integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dset": {
|
||||
"version": "3.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz",
|
||||
|
@ -3548,6 +3656,22 @@
|
|||
"integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/html-to-text": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/html-to-text/-/html-to-text-9.0.5.tgz",
|
||||
"integrity": "sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@selderee/plugin-htmlparser2": "^0.11.0",
|
||||
"deepmerge": "^4.3.1",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"htmlparser2": "^8.0.2",
|
||||
"selderee": "^0.11.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/html-void-elements": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
||||
|
@ -3558,6 +3682,37 @@
|
|||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/htmlparser2/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/http-cache-semantics": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz",
|
||||
|
@ -3817,6 +3972,15 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/leac": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/leac/-/leac-0.6.0.tgz",
|
||||
"integrity": "sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/lilconfig": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz",
|
||||
|
@ -5364,6 +5528,19 @@
|
|||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/parseley": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz",
|
||||
"integrity": "sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"leac": "^0.6.0",
|
||||
"peberminta": "^0.9.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
|
@ -5407,6 +5584,15 @@
|
|||
"integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/peberminta": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/peberminta/-/peberminta-0.9.0.tgz",
|
||||
"integrity": "sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
@ -5600,6 +5786,21 @@
|
|||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
|
||||
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/printable-characters": {
|
||||
"version": "1.0.42",
|
||||
"resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz",
|
||||
|
@ -5673,6 +5874,44 @@
|
|||
"integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz",
|
||||
"integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz",
|
||||
"integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.26.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-promise-suspense": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz",
|
||||
"integrity": "sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-promise-suspense/node_modules/fast-deep-equal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
|
||||
"integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
@ -5965,6 +6204,18 @@
|
|||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/resend": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/resend/-/resend-4.4.1.tgz",
|
||||
"integrity": "sha512-FR22bzMW3VfoyZSBc8ScGo8ShrMWHmWB0G3FrispzWCnYSEEK5M7pyRvZtInKmM/09lsJETKc2q66mX+dXPSmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@react-email/render": "1.0.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
|
@ -6130,6 +6381,25 @@
|
|||
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
|
||||
"integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/selderee": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/selderee/-/selderee-0.11.0.tgz",
|
||||
"integrity": "sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"parseley": "^0.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/killymxi"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
"@astrojs/tailwind": "latest",
|
||||
"astro": "latest",
|
||||
"nodemailer": "^6.10.1",
|
||||
"resend": "^4.4.1",
|
||||
"tailwindcss": "^3.0.24"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
@ -8,7 +8,7 @@ const navItems = [
|
|||
{ name: 'Blog', url: '/blog' },
|
||||
{ name: 'Projects', url: '/projects' },
|
||||
{ name: 'Tech Stack', url: '/tech-stack' },
|
||||
{ name: 'Home Lab', url: 'https://argobox.com' },
|
||||
{ name: 'Home Lab', url: '/homelab' }, // Updated URL
|
||||
{ name: 'Resources', url: '/resources' },
|
||||
{ name: 'About', url: 'https://ArgoBox.com' },
|
||||
{ name: 'Contact', url: 'https://ArgoBox.com/index.html#contact' }
|
||||
|
|
|
@ -1,98 +1,83 @@
|
|||
import type { APIRoute } from 'astro';
|
||||
import nodemailer from 'nodemailer';
|
||||
import { Resend } from 'resend';
|
||||
|
||||
// Validate environment variables
|
||||
const emailUser = import.meta.env.EMAIL_USER;
|
||||
const emailPass = import.meta.env.EMAIL_PASS;
|
||||
const emailTo = import.meta.env.EMAIL_TO || 'daniel.laforce@argobox.com'; // Default recipient
|
||||
// Initialize Resend with API key from environment variables
|
||||
// IMPORTANT: Set RESEND_API_KEY in your deployment environment (e.g., Cloudflare Pages)
|
||||
const resend = new Resend(import.meta.env.RESEND_API_KEY);
|
||||
|
||||
if (!emailUser || !emailPass) {
|
||||
console.error("Email credentials (EMAIL_USER, EMAIL_PASS) are not set in environment variables.");
|
||||
// Optionally, you could throw an error here during build or startup in development
|
||||
// Define the expected structure of the form data
|
||||
interface FormData {
|
||||
name: string;
|
||||
email: string;
|
||||
subject: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
// Configure Nodemailer transporter
|
||||
// Ensure you have configured "less secure app access" in Gmail or use an App Password
|
||||
// if using 2-Factor Authentication. Consider using a more robust email service
|
||||
// like SendGrid, Mailgun, or Resend for production.
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: 'gmail', // Or your preferred service
|
||||
auth: {
|
||||
user: emailUser,
|
||||
pass: emailPass,
|
||||
},
|
||||
});
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
if (!emailUser || !emailPass) {
|
||||
return new Response(JSON.stringify({ message: 'Server email configuration error.' }), {
|
||||
// Check if API key is configured
|
||||
if (!import.meta.env.RESEND_API_KEY) {
|
||||
console.error("Resend API key is not configured.");
|
||||
return new Response(JSON.stringify({ message: 'Server configuration error.' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
let data: FormData;
|
||||
try {
|
||||
data = await request.json();
|
||||
} catch (error) {
|
||||
return new Response(JSON.stringify({ message: 'Invalid request body.' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if (!data.name || !data.email || !data.subject || !data.message) {
|
||||
return new Response(JSON.stringify({ message: 'Missing required fields.' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await request.json();
|
||||
const { name, email, subject, message } = data;
|
||||
// Send email using Resend
|
||||
const { data: emailData, error: emailError } = await resend.emails.send({
|
||||
from: 'Contact Form <noreply@argobox.com>', // Replace with your desired "from" address (must be a verified domain in Resend)
|
||||
to: ['daniel.laforce@gmail.com'], // Replace with the email address where you want to receive messages
|
||||
subject: `New Contact Form Submission: ${data.subject}`,
|
||||
html: `
|
||||
<h1>New Contact Form Submission</h1>
|
||||
<p><strong>Name:</strong> ${data.name}</p>
|
||||
<p><strong>Email:</strong> ${data.email}</p>
|
||||
<p><strong>Subject:</strong> ${data.subject}</p>
|
||||
<hr>
|
||||
<p><strong>Message:</strong></p>
|
||||
<p>${data.message.replace(/\n/g, '<br>')}</p>
|
||||
`,
|
||||
reply_to: data.email, // Set the reply-to header to the sender's email
|
||||
});
|
||||
|
||||
// Basic validation
|
||||
if (!name || !email || !subject || !message) {
|
||||
return new Response(JSON.stringify({ message: 'Missing required fields.' }), {
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
if (emailError) {
|
||||
console.error('Resend error:', emailError);
|
||||
return new Response(JSON.stringify({ message: 'Failed to send email.' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Email options
|
||||
const mailOptions = {
|
||||
from: `"${name}" <${emailUser}>`, // Display name and authorized sender
|
||||
replyTo: email, // Set reply-to for easy response
|
||||
to: emailTo, // Recipient address from env or default
|
||||
subject: `Argobox Contact Form: ${subject}`,
|
||||
text: `
|
||||
Name: ${name}
|
||||
Email: ${email}
|
||||
Subject: ${subject}
|
||||
|
||||
Message:
|
||||
${message}
|
||||
`,
|
||||
html: `
|
||||
<h3>New Argobox Contact Form Submission</h3>
|
||||
<p><strong>Name:</strong> ${name}</p>
|
||||
<p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p>
|
||||
<p><strong>Subject:</strong> ${subject}</p>
|
||||
<h4>Message:</h4>
|
||||
<p>${message.replace(/\n/g, '<br>')}</p>
|
||||
`
|
||||
};
|
||||
|
||||
// Send email
|
||||
await transporter.sendMail(mailOptions);
|
||||
|
||||
return new Response(JSON.stringify({ message: 'Message sent successfully!' }), {
|
||||
// Email sent successfully
|
||||
return new Response(JSON.stringify({ message: 'Email sent successfully!' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error processing contact form:', error);
|
||||
let errorMessage = 'Failed to send message. Please try again later.';
|
||||
if (error instanceof Error) {
|
||||
// Check for specific Nodemailer errors if needed
|
||||
// e.g., if (error.code === 'EAUTH') errorMessage = 'Server authentication error.';
|
||||
}
|
||||
return new Response(JSON.stringify({ message: errorMessage }), {
|
||||
console.error('Unexpected error sending email:', error);
|
||||
return new Response(JSON.stringify({ message: 'An unexpected server error occurred.' }), {
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Optional: Add a GET handler if you want to test the endpoint directly
|
||||
// export const GET: APIRoute = () => {
|
||||
// return new Response(JSON.stringify({ message: 'Contact API endpoint is active. Use POST to submit.' }), {
|
||||
// status: 200,
|
||||
// headers: { 'Content-Type': 'application/json' }
|
||||
// });
|
||||
// };
|
|
@ -0,0 +1,203 @@
|
|||
---
|
||||
// src/pages/contact.astro
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
|
||||
const title = "Contact | ArgoBox";
|
||||
const description = "Get in touch with ArgoBox. Send a message using the contact form.";
|
||||
---
|
||||
|
||||
<BaseLayout {title} {description}>
|
||||
<Header slot="header" />
|
||||
|
||||
<main class="contact-page container">
|
||||
<section class="contact-form-section">
|
||||
<h1>Contact Us</h1>
|
||||
<p>Have a question, suggestion, or just want to say hello? Fill out the form below.</p>
|
||||
|
||||
<form id="contact-form" class="contact-form">
|
||||
<div class="form-group">
|
||||
<label for="name">Name</label>
|
||||
<input type="text" id="name" name="name" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="subject">Subject</label>
|
||||
<input type="text" id="subject" name="subject" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="message">Message</label>
|
||||
<textarea id="message" name="message" rows="6" required></textarea>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" id="submit-button">
|
||||
Send Message
|
||||
</button>
|
||||
<div id="form-status" class="form-status" aria-live="polite"></div>
|
||||
</form>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
.contact-page {
|
||||
padding-top: 2rem;
|
||||
padding-bottom: 4rem;
|
||||
max-width: 700px; /* Limit width for better readability */
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.contact-form-section h1 {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.contact-form-section p {
|
||||
text-align: center;
|
||||
margin-bottom: 2.5rem;
|
||||
color: var(--text-secondary);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.contact-form {
|
||||
background: var(--bg-secondary);
|
||||
padding: 2.5rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border-primary);
|
||||
box-shadow: var(--card-shadow-lg);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.form-group input[type="text"],
|
||||
.form-group input[type="email"],
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 0.8rem 1rem;
|
||||
border: 1px solid var(--border-primary);
|
||||
border-radius: 6px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px var(--glow-primary);
|
||||
}
|
||||
|
||||
.form-group textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
display: inline-block;
|
||||
padding: 0.8rem 1.5rem;
|
||||
background: var(--accent-primary);
|
||||
color: var(--bg-primary);
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease, transform 0.1s ease;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-secondary);
|
||||
}
|
||||
.btn-primary:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-status {
|
||||
margin-top: 1.5rem;
|
||||
padding: 1rem;
|
||||
border-radius: 6px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-status.success {
|
||||
background-color: rgba(16, 185, 129, 0.1); /* Tailwind green-100 */
|
||||
color: #059669; /* Tailwind green-700 */
|
||||
border: 1px solid rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
.form-status.error {
|
||||
background-color: rgba(239, 68, 68, 0.1); /* Tailwind red-100 */
|
||||
color: #dc2626; /* Tailwind red-600 */
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const form = document.getElementById('contact-form') as HTMLFormElement;
|
||||
const formStatus = document.getElementById('form-status');
|
||||
const submitButton = document.getElementById('submit-button') as HTMLButtonElement;
|
||||
|
||||
form?.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
if (!formStatus || !submitButton) return;
|
||||
|
||||
submitButton.disabled = true;
|
||||
submitButton.textContent = 'Sending...';
|
||||
formStatus.textContent = '';
|
||||
formStatus.className = 'form-status'; // Reset classes
|
||||
|
||||
const formData = new FormData(form);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/contact', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
formStatus.textContent = 'Message sent successfully! Thank you.';
|
||||
formStatus.classList.add('success');
|
||||
form.reset(); // Clear the form
|
||||
} else {
|
||||
formStatus.textContent = `Error: ${result.message || 'Could not send message.'}`;
|
||||
formStatus.classList.add('error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Form submission error:', error);
|
||||
formStatus.textContent = 'An unexpected error occurred. Please try again later.';
|
||||
formStatus.classList.add('error');
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
submitButton.textContent = 'Send Message';
|
||||
}
|
||||
});
|
||||
</script>
|
Loading…
Reference in New Issue