cloudflare deployment
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
# Build output
|
# Build output
|
||||||
dist/
|
dist/
|
||||||
.astro/
|
.astro/
|
||||||
|
.wrangler/
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
node_modules/
|
node_modules/
|
||||||
|
|||||||
@@ -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
873
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
5
wrangler.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "cachetdeco",
|
||||||
|
"pages_build_output_dir": "./dist",
|
||||||
|
"compatibility_date": "2025-10-08"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user