cloudflare deployment

This commit is contained in:
2026-04-03 11:19:29 -04:00
parent 5dc33897ce
commit 84728c9532
9 changed files with 896 additions and 39 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# Build output # Build output
dist/ dist/
.astro/ .astro/
.wrangler/
# Dependencies # Dependencies
node_modules/ node_modules/

View File

@@ -2,11 +2,11 @@ import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite'; import tailwindcss from '@tailwindcss/vite';
import sitemap from '@astrojs/sitemap'; import sitemap from '@astrojs/sitemap';
import react from '@astrojs/react'; import react from '@astrojs/react';
import node from '@astrojs/node'; import cloudflare from '@astrojs/cloudflare';
export default defineConfig({ export default defineConfig({
output: 'server', output: 'server',
adapter: node({ mode: 'standalone' }), adapter: cloudflare(),
site: 'https://cachetdeco.com', site: 'https://cachetdeco.com',
i18n: { i18n: {
defaultLocale: 'fr', defaultLocale: 'fr',

873
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@
"astro": "astro" "astro": "astro"
}, },
"dependencies": { "dependencies": {
"@astrojs/node": "^10.0.4", "@astrojs/cloudflare": "^13.1.7",
"@astrojs/react": "^5.0.2", "@astrojs/react": "^5.0.2",
"@astrojs/sitemap": "^3.7.2", "@astrojs/sitemap": "^3.7.2",
"@fontsource-variable/geist": "^5.2.8", "@fontsource-variable/geist": "^5.2.8",

View File

@@ -1,4 +1,4 @@
import { useState } from "react"; import { useState, useEffect, useRef } from "react";
import { motion } from "motion/react"; import { motion } from "motion/react";
import { Dialog, DialogPanel } from "@headlessui/react"; import { Dialog, DialogPanel } from "@headlessui/react";
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline"; import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
@@ -16,18 +16,41 @@ export default function Header({
ctaHref: string; ctaHref: string;
}) { }) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [isVisible, setIsVisible] = useState(true);
const [isScrolled, setIsScrolled] = useState(false);
const lastScrollY = useRef(0);
const phoneHref = `tel:${general.phone.replace(/\D/g, "")}`; const phoneHref = `tel:${general.phone.replace(/\D/g, "")}`;
useEffect(() => {
const handleScroll = () => {
const currentY = window.scrollY;
const delta = currentY - lastScrollY.current;
if (delta > 8 && currentY > 80) {
setIsVisible(false);
} else if (delta < -8) {
setIsVisible(true);
}
setIsScrolled(currentY > 10);
lastScrollY.current = currentY;
};
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, []);
return ( return (
<header className="absolute inset-x-0 top-0 z-50"> <header
className={`fixed inset-x-0 top-0 z-50 transition-all duration-300 ease-in-out`}
style={{ transform: isVisible ? "translateY(0)" : "translateY(-100%)" }}
>
<nav <nav
aria-label="Global" aria-label="Global"
className="flex items-center justify-between p-6 lg:px-8" className={`flex items-center justify-between p-6 lg:px-8 ${isScrolled ? "bg-[#314732]/95 shadow-md backdrop-blur-sm py-4" : ""} `}
> >
<div className="flex lg:flex-1"> <div className="flex lg:flex-1">
<a <a
href="/" href="/"
className="flex items-center justify-center bg-[white] rounded-full shadow-md p-2 lg:translate-y-1.5" className="flex items-center justify-center bg-[white] rounded-full shadow-md p-2"
> >
<div className="rounded-full "> <div className="rounded-full ">
<span className="sr-only">{general.siteName}</span> <span className="sr-only">{general.siteName}</span>
@@ -56,7 +79,7 @@ export default function Header({
transition={{ type: "spring", stiffness: 400, damping: 25 }} transition={{ type: "spring", stiffness: 400, damping: 25 }}
key={item.name} key={item.name}
href={item.href} href={item.href}
className="text-sm/6 font-semibold text-white hover:underline underline-offset-4 decoration-2 drop-shadow" className="text-base/6 font-semibold text-white hover:underline underline-offset-4 decoration-2 drop-shadow"
> >
{item.name} {item.name}
</motion.a> </motion.a>

View File

@@ -36,7 +36,7 @@ const navigationItems = nav.links.map((link) => ({ name: link.name, href: link.h
navigation={navigationItems} navigation={navigationItems}
ctaLabel={nav.cta.label} ctaLabel={nav.cta.label}
ctaHref={nav.cta.href} ctaHref={nav.cta.href}
client:load client:only="react"
/> />
<main id="main-content"> <main id="main-content">
<slot /> <slot />

View File

@@ -70,7 +70,7 @@ function buildContactNotificationHtml(data: ContactBody): string {
</html>`; </html>`;
} }
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request, locals }) => {
let body: unknown; let body: unknown;
try { try {
body = await request.json(); body = await request.json();
@@ -88,7 +88,8 @@ export const POST: APIRoute = async ({ request }) => {
}); });
} }
const apiKey = process.env.RESEND_API_KEY; const runtime = (locals as { runtime?: { env?: Record<string, string> } }).runtime;
const apiKey = runtime?.env?.RESEND_API_KEY;
const toAddress = general.emailToContact?.trim(); const toAddress = general.emailToContact?.trim();
const fromAddress = general.emailFromContact?.trim(); const fromAddress = general.emailFromContact?.trim();
if (!apiKey || !toAddress || !fromAddress) { if (!apiKey || !toAddress || !fromAddress) {

View File

@@ -1,4 +1,3 @@
import { Buffer } from 'node:buffer';
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
import type { EmailAttachment } from '@/lib/email'; import type { EmailAttachment } from '@/lib/email';
import { sendEmail } from '@/lib/email'; import { sendEmail } from '@/lib/email';
@@ -144,7 +143,7 @@ function buildNotificationHtml(data: SubmissionPayload, attachmentNames: string[
</html>`; </html>`;
} }
export const POST: APIRoute = async ({ request }) => { export const POST: APIRoute = async ({ request, locals }) => {
const contentType = request.headers.get('content-type') ?? ''; const contentType = request.headers.get('content-type') ?? '';
if (!contentType.includes('multipart/form-data')) { if (!contentType.includes('multipart/form-data')) {
return new Response(JSON.stringify({ success: false, error: 'Invalid Content-Type.' }), { return new Response(JSON.stringify({ success: false, error: 'Invalid Content-Type.' }), {
@@ -206,13 +205,16 @@ export const POST: APIRoute = async ({ request }) => {
const safeName = sanitizeFilename(file.name); const safeName = sanitizeFilename(file.name);
attachmentNames.push(safeName); attachmentNames.push(safeName);
const buf = await file.arrayBuffer(); const buf = await file.arrayBuffer();
const bytes = new Uint8Array(buf);
const binary = bytes.reduce((acc, b) => acc + String.fromCharCode(b), '');
attachments.push({ attachments.push({
filename: safeName, filename: safeName,
content: Buffer.from(buf).toString('base64'), content: btoa(binary),
}); });
} }
const apiKey = process.env.RESEND_API_KEY; const runtime = (locals as { runtime?: { env?: Record<string, string> } }).runtime;
const apiKey = runtime?.env?.RESEND_API_KEY;
const fromAddress = general.fromAddressSubmission?.trim(); const fromAddress = general.fromAddressSubmission?.trim();
const toAddress = general.toAddressSubmission?.trim(); const toAddress = general.toAddressSubmission?.trim();

5
wrangler.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "cachetdeco",
"pages_build_output_dir": "./dist",
"compatibility_date": "2025-10-08"
}