Archived
Rebrand to Sage & Cream identity, working contact form, drop Firebase
- New sage/cream palette and stacked logo lockup (The Vine over Coffeehouse + Bakery) driven by currentColor; logo SVGs now colorable - Reuse the logo component for the hero and section marks - Rewrite site copy: real founding (Morissa Bennett, 2024), real story from the product range, remove invented claims and AI phrasing - Contact form now sends over SMTP via /api/contact (nodemailer) with real send/error states, server-side validation, and reply-to the customer; delivers to CONTACT_TO. Add .env.example and a test-email script - Fix mobile: unmount the off-screen menu (killed sideways scroll), cap logo width, responsive heroes and type - Remove Firebase (config, tracked build output, placeholder pages, nix firebase-tools) now that hosting has moved - Delete dead code: LoyaltyCardForm, ProductModal, LoadingSpinner, card-flip CSS, unused Playfair font - Serve dev/start on port 2024
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
// Server-side only, so the SMTP credentials never reach the browser.
|
||||
const { SMTP_SERVER, SMTP_PORT, SMTP_USERNAME, SMTP_TOKEN, CONTACT_TO, CONTACT_FROM } = process.env;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
if (!SMTP_SERVER || !SMTP_PORT || !SMTP_USERNAME || !SMTP_TOKEN) {
|
||||
console.error('contact: SMTP env vars missing');
|
||||
return NextResponse.json({ error: 'Email is not configured.' }, { status: 500 });
|
||||
}
|
||||
|
||||
let body: { name?: string; email?: string; message?: string };
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Invalid request.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const name = body.name?.trim();
|
||||
const email = body.email?.trim();
|
||||
const message = body.message?.trim();
|
||||
|
||||
// Trust boundary: validate before handing anything to the mailer.
|
||||
if (!name || !email || !message) {
|
||||
return NextResponse.json({ error: 'Please fill in every field.' }, { status: 400 });
|
||||
}
|
||||
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return NextResponse.json({ error: 'That email address does not look right.' }, { status: 400 });
|
||||
}
|
||||
if (message.length > 5000) {
|
||||
return NextResponse.json({ error: 'That message is too long.' }, { status: 400 });
|
||||
}
|
||||
|
||||
const port = Number(SMTP_PORT);
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: SMTP_SERVER,
|
||||
port,
|
||||
secure: port === 465, // 465 is implicit TLS; 587 upgrades via STARTTLS
|
||||
auth: { user: SMTP_USERNAME, pass: SMTP_TOKEN },
|
||||
});
|
||||
|
||||
try {
|
||||
await transporter.sendMail({
|
||||
from: CONTACT_FROM || SMTP_USERNAME,
|
||||
to: CONTACT_TO || '[email protected]',
|
||||
replyTo: `${name} <${email}>`, // so hitting reply answers the customer
|
||||
subject: `Website enquiry from ${name}`,
|
||||
text: `${message}\n\nFrom: ${name} <${email}>`,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('contact: send failed', err);
|
||||
return NextResponse.json({ error: 'Could not send the message.' }, { status: 502 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true });
|
||||
}
|
||||
+10
-30
@@ -4,14 +4,22 @@
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
/* Nothing on this site is meant to scroll sideways. */
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-bakery-50 text-bakery-900;
|
||||
overflow-x: hidden;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.5s ease-in forwards;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html { scroll-behavior: auto; }
|
||||
body { animation: none; opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -29,34 +37,6 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* Card flip styles */
|
||||
.card-container {
|
||||
perspective: 1500px;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
transition: transform 0.8s;
|
||||
transform-style: preserve-3d;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: rotateY(180deg);
|
||||
}
|
||||
|
||||
.card-side {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
backface-visibility: hidden;
|
||||
-webkit-backface-visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
.card-back {
|
||||
transform: rotateY(180deg);
|
||||
::selection {
|
||||
@apply bg-bakery-300 text-bakery-900;
|
||||
}
|
||||
+8
-14
@@ -1,17 +1,11 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Playfair_Display, Raleway } from "next/font/google";
|
||||
import { Raleway } from "next/font/google";
|
||||
import localFont from 'next/font/local';
|
||||
import "./globals.css";
|
||||
import Header from '@/components/Header';
|
||||
import Footer from '@/components/Footer';
|
||||
|
||||
const playfair = Playfair_Display({
|
||||
subsets: ["latin"],
|
||||
variable: '--font-playfair',
|
||||
display: 'swap',
|
||||
});
|
||||
|
||||
const raleway = Raleway({
|
||||
const raleway = Raleway({
|
||||
subsets: ["latin"],
|
||||
variable: '--font-raleway',
|
||||
display: 'swap',
|
||||
@@ -30,13 +24,13 @@ const lejour = localFont({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "The Vine Coffeehouse & Bakery",
|
||||
description: "Artisanal bakery and coffeehouse in Princeville, IL offering traditional breads, pastries, and cakes made with quality ingredients.",
|
||||
keywords: "bakery, coffeehouse, artisanal bread, pastries, cakes, Princeville IL",
|
||||
title: "The Vine Coffeehouse + Bakery",
|
||||
description: "A locally owned coffeehouse and bakery in downtown Princeville, Illinois. We bake pastries, custom cakes, cookies, and cinnamon rolls, and serve sandwiches, paninis, and coffee.",
|
||||
keywords: "bakery, coffeehouse, pastries, custom cakes, cinnamon rolls, paninis, Princeville IL",
|
||||
metadataBase: new URL('https://itsthevine.com'),
|
||||
openGraph: {
|
||||
title: 'The Vine Coffeehouse & Bakery',
|
||||
description: 'Discover The Vine Coffeehouse and Bakery in Princeville, IL',
|
||||
title: 'The Vine Coffeehouse + Bakery',
|
||||
description: 'A locally owned coffeehouse and bakery in downtown Princeville, IL.',
|
||||
locale: 'en_US',
|
||||
type: 'website',
|
||||
},
|
||||
@@ -62,7 +56,7 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en" className={`${playfair.variable} ${raleway.variable} ${adbhashitha.variable} ${lejour.variable}`}>
|
||||
<html lang="en" className={`${raleway.variable} ${adbhashitha.variable} ${lejour.variable}`}>
|
||||
<body className="min-h-screen bg-bakery-50 flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-grow">
|
||||
|
||||
Reference in New Issue
Block a user