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:
Austin
2026-07-14 17:48:00 -05:00
parent f4ce93965b
commit 5c81b65e51
170 changed files with 980 additions and 1399 deletions
+57
View File
@@ -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
View File
@@ -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
View File
@@ -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">
+57 -43
View File
@@ -1,15 +1,16 @@
'use client';
import { useState } from 'react';
import { motion } from 'framer-motion';
import LoadingSpinner from './LoadingSpinner';
type Status = 'idle' | 'sending' | 'sent' | 'error';
const ContactPage = () => {
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const [status, setStatus] = useState<Status>('idle');
const [error, setError] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
setFormData({
@@ -18,87 +19,100 @@ const ContactPage = () => {
});
};
// TODO: Implement form submission
const handleSubmit = (e: React.FormEvent) => {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
// Simulate form submission
setTimeout(() => {
setIsLoading(false);
alert('Message sent!');
setStatus('sending');
setError('');
try {
const res = await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || 'Could not send the message.');
setStatus('sent');
setFormData({ name: '', email: '', message: '' });
}, 1000);
} catch (err) {
// Never claim success we did not get. Tell them, and give them the phone number.
setStatus('error');
setError(err instanceof Error ? err.message : 'Could not send the message.');
}
};
return (
<div className="min-h-screen bg-bakery-50">
{/* Hero Section */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="relative h-[40vh] bg-bakery-600"
>
<div className="absolute inset-0 bg-black/40 bg-cover bg-center" style={{ backgroundImage: 'url(/images/ressources/contact-bg.jpeg)', opacity: '0.6' }} />
<div className="relative container mx-auto px-4 h-[45vh] flex items-center justify-center text-center">
<div>
<h1 className="font-sans text-4xl md:text-5xl text-white mb-4">
Contact Us
</h1>
<p className="text-white/90 text-lg max-w-2xl">
We would love to hear from you! Please fill out the form below to get in touch.
</p>
</div>
</div>
</motion.div>
{/* Page header */}
<header className="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
<h1 className="font-adbhashitha text-4xl md:text-5xl text-bakery-900">
Contact us
</h1>
</header>
{/* Contact Form Section */}
<div className="container mx-auto px-4 py-16">
<div className="max-w-2xl mx-auto bg-white p-8 rounded-lg shadow-md">
<h2 className="text-2xl font-semibold mb-6">Get in Touch</h2>
<div className="container mx-auto px-4 pb-16 md:pb-24">
<div className="max-w-2xl mx-auto bg-white p-8 md:p-10 rounded-3xl shadow-sm">
<h2 className="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-2">Get in touch</h2>
<p className="text-bakery-700 mb-8">
Or call us: <a href="tel:+13097010660" className="underline underline-offset-4 hover:text-bakery-600">(309) 701-0660</a>
</p>
<form onSubmit={handleSubmit}>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2" htmlFor="name">Name</label>
<label className="block text-sm font-medium text-bakery-800 mb-2" htmlFor="name">Name</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-bakery-600"
className="w-full px-4 py-2.5 bg-bakery-50 border border-bakery-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-bakery-500 focus:border-bakery-500"
required
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2" htmlFor="email">Email</label>
<label className="block text-sm font-medium text-bakery-800 mb-2" htmlFor="email">Email</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleChange}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-bakery-600"
className="w-full px-4 py-2.5 bg-bakery-50 border border-bakery-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-bakery-500 focus:border-bakery-500"
required
/>
</div>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2" htmlFor="message">Message</label>
<label className="block text-sm font-medium text-bakery-800 mb-2" htmlFor="message">Message</label>
<textarea
id="message"
name="message"
value={formData.message}
onChange={handleChange}
className="w-full px-4 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-bakery-600"
className="w-full px-4 py-2.5 bg-bakery-50 border border-bakery-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-bakery-500 focus:border-bakery-500"
rows={5}
required
/>
</div>
<div className="flex justify-center">
<button
type="submit"
className="px-6 py-2 bg-bakery-600 text-white rounded-full hover:bg-bakery-700 transition-colors"
<div className="flex flex-col items-center gap-4">
<button
type="submit"
disabled={status === 'sending'}
className="px-8 py-3 bg-bakery-600 text-white rounded-full tracking-wide hover:bg-bakery-700 transition-colors disabled:opacity-60"
>
{isLoading ? <LoadingSpinner /> : 'Send Message'}
{status === 'sending' ? 'Sending...' : 'Send message'}
</button>
{status === 'sent' && (
<p role="status" className="text-bakery-700">
Thanks. Your message is on its way, and we will get back to you.
</p>
)}
{status === 'error' && (
<p role="alert" className="text-center text-red-700">
{error} Please call us on{' '}
<a href="tel:+13097010660" className="underline underline-offset-4">(309) 701-0660</a>.
</p>
)}
</div>
</form>
</div>
+21 -26
View File
@@ -3,55 +3,50 @@ import Logo from './Logo';
const Footer = () => {
return (
<footer className="bg-bakery-800 text-white">
<div className="container mx-auto px-4 py-12">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Logo and Description */}
<div className="col-span-1 md:col-span-2 flex items-center">
<Logo logoColor="#f2f3f2"/>
<footer className="bg-bakery-900 text-bakery-100">
<div className="container mx-auto px-4 py-14">
<div className="grid grid-cols-1 md:grid-cols-4 gap-10">
{/* Logo */}
<div className="col-span-1 md:col-span-2 flex items-start">
<Logo className="text-bakery-50" />
</div>
{/* Navigation Links */}
<div>
<h3 className="font-adbhashitha text-xl mb-4">Navigation</h3>
<h3 className="font-adbhashitha text-sm uppercase tracking-[0.18em] text-bakery-300 mb-4">Navigation</h3>
<ul className="space-y-2">
<li>
<Link href="/products" className="text-bakery-200 hover:text-white transition">
Our Products
</Link>
<Link href="/products" className="hover:text-white transition">Our Products</Link>
</li>
<li>
<Link href="/history" className="text-bakery-200 hover:text-white transition">
History
</Link>
<Link href="/history" className="hover:text-white transition">Our Story</Link>
</li>
<li>
<Link href="/contact" className="text-bakery-200 hover:text-white transition">
Contact
</Link>
<Link href="/contact" className="hover:text-white transition">Contact</Link>
</li>
</ul>
</div>
{/* Contact Info */}
<div>
<h3 className="font-adbhashitha text-xl mb-4">Contact</h3>
<ul className="space-y-2 text-bakery-200">
<li>215 E Main Street</li>
<li>Princeville, IL 61559</li>
<li>(309) 701-0660</li>
<li>contact@itsthevine.com</li>
</ul>
<h3 className="font-adbhashitha text-sm uppercase tracking-[0.18em] text-bakery-300 mb-4">Visit</h3>
<address className="not-italic space-y-2">
<p>215 E Main Street<br />Princeville, IL 61559</p>
<p><a href="tel:+13097010660" className="hover:text-white transition">(309) 701-0660</a></p>
<p className="break-words">
<a href="mailto:[email protected]" className="hover:text-white transition">contact@itsthevine.com</a>
</p>
</address>
</div>
</div>
{/* Bottom Bar */}
<div className="border-t border-bakery-700 mt-8 pt-8 text-center text-bakery-300">
<p>© 2025 The Vine Cofeehouse & Bakery</p>
<div className="mt-12 text-center text-sm text-bakery-300">
<p>© {new Date().getFullYear()} The Vine Coffeehouse + Bakery</p>
</div>
</div>
</footer>
);
};
export default Footer;
export default Footer;
+57 -46
View File
@@ -1,5 +1,6 @@
'use client'
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import Link from 'next/link';
import Logo from './Logo';
@@ -8,23 +9,23 @@ const Header = () => {
const navItems = [
{ label: 'Our Products', href: '/products' },
// { label: 'History', href: '/history' },
// { label: 'Contact', href: '/contact' },
{ label: 'Our Story', href: '/history' },
{ label: 'Contact', href: '/contact' },
];
return (
<header className="bg-white shadow-sm relative">
<header className="sticky top-0 z-40 bg-bakery-50/90 backdrop-blur border-b border-bakery-200">
<div className="container mx-auto px-4">
<div className="flex items-center justify-between h-20">
<div className="flex items-center justify-between gap-2 h-20 md:h-24">
{/* Logo */}
<Logo logoColor="#40433c"/>
<Logo className="text-bakery-700" />
{/* Desktop Navigation */}
<nav className="hidden md:flex space-x-8">
<nav className="hidden md:flex items-center gap-8">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className="text-bakery-700 hover:text-bakery-900 transition"
className="text-sm uppercase tracking-[0.15em] text-bakery-700 hover:text-bakery-900 transition"
>
{item.label}
</Link>
@@ -32,10 +33,11 @@ const Header = () => {
</nav>
{/* Mobile menu button */}
<button
className="md:hidden p-2"
<button
className="md:hidden p-2 shrink-0"
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
aria-label="Toggle menu"
aria-expanded={isMobileMenuOpen}
>
<svg
className="h-6 w-6 text-bakery-700"
@@ -51,43 +53,52 @@ const Header = () => {
</button>
</div>
{/* Mobile Navigation */}
<div className={`
md:hidden fixed inset-0 bg-white z-50 transform transition-transform duration-300 ease-in-out
${isMobileMenuOpen ? 'translate-x-0' : 'translate-x-full'}
`}>
<div className="p-4">
<div className="flex justify-between items-center mb-8">
<Logo logoColor='black'/>
<button
onClick={() => setIsMobileMenuOpen(false)}
className="p-2"
aria-label="Close menu"
>
<svg
className="h-6 w-6 text-bakery-700"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<nav className="flex flex-col space-y-4">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className="text-bakery-700 hover:text-bakery-900 transition py-2 text-lg"
onClick={() => setIsMobileMenuOpen(false)}
>
{item.label}
</Link>
))}
</nav>
</div>
</div>
{/* Mobile Navigation — must UNMOUNT when closed. A panel parked off-screen
at translate-x-full still extends the scrollable area, which is what let
you scroll sideways and find the menu. */}
<AnimatePresence>
{isMobileMenuOpen && (
<motion.div
initial={{ x: '100%' }}
animate={{ x: 0 }}
exit={{ x: '100%' }}
transition={{ type: 'tween', duration: 0.25, ease: 'easeOut' }}
className="md:hidden fixed inset-0 bg-bakery-50 z-50"
>
<div className="p-4">
<div className="flex justify-between items-center gap-2 mb-8 h-16">
<Logo className="text-bakery-800" />
<button
onClick={() => setIsMobileMenuOpen(false)}
className="p-2 shrink-0"
aria-label="Close menu"
>
<svg
className="h-6 w-6 text-bakery-700"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<nav className="flex flex-col">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className="text-bakery-800 hover:text-bakery-600 transition py-4 text-lg"
onClick={() => setIsMobileMenuOpen(false)}
>
{item.label}
</Link>
))}
</nav>
</div>
</motion.div>
)}
</AnimatePresence>
</div>
</header>
);
+48 -37
View File
@@ -1,44 +1,55 @@
'use client';
import { motion } from 'framer-motion';
const HistoryPage = () => {
return (
<div className="min-h-screen bg-bakery-50">
{/* Hero Section */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="relative h-[40vh] bg-bakery-600"
>
<div className="absolute inset-0 bg-black/40 bg-cover bg-center" style={{ backgroundImage: 'url(/images/ressources/history-bg.jpeg)', opacity: '0.6' }} />
<div className="relative container mx-auto px-4 h-[45vh] flex items-center justify-center text-center">
<div>
<h1 className="font-sans text-4xl md:text-5xl text-white mb-4">
Our History
</h1>
<p className="text-white/90 text-lg max-w-2xl">
Discover the story behind our bakery.
</p>
</div>
</div>
</motion.div>
<div className="bg-bakery-50">
{/* Page header */}
<header className="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
<h1 className="font-adbhashitha text-4xl md:text-5xl text-bakery-900">
Our story
</h1>
</header>
{/* History Content Section */}
<div className="container mx-auto px-4 py-16">
<div className="max-w-3xl mx-auto bg-white p-8 rounded-lg shadow-md">
<h2 className="text-2xl font-semibold mb-6">Our Journey</h2>
<p className="mb-4">
Our bakery was founded in 1920 by John Doe, a passionate baker with a dream to bring the finest baked goods to the community. Over the years, our bakery has grown and evolved, but our commitment to quality and tradition has remained the same.
</p>
<p className="mb-4">
In the early days, John would wake up before dawn to prepare fresh bread and pastries for the day. His dedication and hard work quickly earned him a loyal customer base. As the bakerys reputation grew, so did its offerings. Today, we offer a wide variety of baked goods, from classic breads and pastries to modern cakes and desserts.
</p>
<p className="mb-4">
Throughout the decades, our bakery has remained a family-owned business. Each generation has brought new ideas and innovations, while staying true to the values and traditions that John established. We are proud to be a part of this community and to continue serving our customers with the same passion and dedication that started it all.
</p>
<p className="mb-4">
Thank you for being a part of our journey. We look forward to many more years of baking for you and your family.
</p>
{/* Story */}
<div className="container mx-auto px-4 pb-16 md:pb-24">
<div className="max-w-2xl mx-auto space-y-12">
<section>
<h2 className="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-4">How it started</h2>
<p className="text-bakery-800 leading-relaxed">
Morissa Bennett opened The Vine in 2024, at 215 E Main Street, in the middle of downtown
Princeville. The plan was not complicated. Bake it ourselves, sell it ourselves, and keep
enough tables that nobody feels rushed out the door.
</p>
</section>
<section>
<h2 className="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-4">What we make</h2>
<p className="text-bakery-800 leading-relaxed">
We opened with coffee and pastries. The menu kept growing. Now there are cinnamon rolls
and caramel rolls, scones, cookie bars, macarons, brownies, and pies, plus sandwiches and
paninis once the lunch crowd shows up.
</p>
</section>
<section>
<h2 className="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-4">The cakes are the fun part</h2>
<p className="text-bakery-800 leading-relaxed">
Cakes and decorated cookies are made to order, which means we mostly bake whatever
Princeville is celebrating that week. We have done a tractor, a cow, a 76th birthday, a
retirement, a wedding, and a cake for the class of 1964. We have iced sugar cookies for
the cross country team and for a bridal party. If you can describe it, we will have a go
at it.
</p>
</section>
<section>
<h2 className="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-4">Around town</h2>
<p className="text-bakery-800 leading-relaxed">
We turn out for Christmas in the Village every year and for other civic events, and the
Princeville Civic Association counts us among the town&apos;s small businesses. Enjoy
Illinois and Discover Peoria have both pointed travelers our way. If you are one of them,
we open at 7:00am, Tuesday through Saturday.
</p>
</section>
</div>
</div>
</div>
+82 -69
View File
@@ -1,69 +1,77 @@
'use client'
import Link from 'next/link';
// import { useState } from 'react';
import Image from 'next/image';
import Logo from './Logo';
const HomePage = () => {
// const [showLoyaltyForm, setShowLoyaltyForm] = useState(false);
return (
<div className="min-h-screen">
{/* Hero Section */}
<section className="relative py-16 bg-bakery-600 text-white">
{/* Background Image */}
{/* <Image
<div>
{/* Hero — centred lockup on a sage wash. */}
<section className="relative flex items-center min-h-[78svh] py-20 md:py-28 bg-bakery-900 text-white overflow-hidden">
{/* Softened on purpose: the storefront's own painted sign sits right behind
the logo, so a sharp photo makes you read the name twice. scale-110 hides
the blur's feathered edges. */}
<Image
src="/images/gallery/Outside.jpg"
alt="The Vine Coffeehouse & Bakery"
alt=""
fill
priority
className="object-cover"
quality={90}
/> */}
{/* Overlay */}
<div className="absolute inset-0"/>
{/* Content */}
<div className="relative container mx-auto px-4 h-full flex items-center">
<div className="text-white max-w-2xl">
<div className="w-full">
<img
src="/images/vine_logo/linen_mist.png"
alt="The Vine Coffeehouse & Bakery"
className="object-cover"
/>
</div>
<p className="text-xl mb-8">
Discover our homemade treats and cakes made with passion.
</p>
<Link
href="/products"
className="bg-bakery-50 hover:bg-bakery-100 text-bakery-800 px-8 py-3 rounded-md inline-block transition"
className="object-cover object-bottom blur-[3px] scale-110"
/>
{/* Sage wash rather than a neutral black scrim — the tint is the identity. */}
<div className="absolute inset-0 bg-bakery-900/80" />
<div className="absolute inset-0 bg-gradient-to-t from-bakery-900 via-bakery-800/60 to-bakery-900/80" />
{/* w-full/min-w-0 keep this flex item from sizing to its max-content
width and blowing out the page on narrow screens. */}
<div className="relative container mx-auto px-4 w-full min-w-0">
<div className="max-w-3xl mx-auto text-center">
<Logo size="lg" linked={false} className="text-bakery-50 mb-10 justify-center" />
<p className="text-lg sm:text-xl text-bakery-100 mb-10 text-balance leading-relaxed">
A coffeehouse and bakery in downtown Princeville, Illinois.
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Link
href="/products"
className="bg-bakery-50 hover:bg-white text-bakery-900 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition shadow-lg"
>
See Our Products
See the menu
</Link>
<a
href="#visit"
className="border border-bakery-200/60 hover:bg-bakery-50/10 text-bakery-50 px-8 py-3.5 rounded-full font-medium tracking-wide inline-block transition"
>
Visit us
</a>
</div>
</div>
</div>
</section>
{/* Our Specialties */}
<section className="py-16 bg-white">
<section className="py-16 md:py-24 bg-bakery-50">
<div className="container mx-auto px-4">
<h2 className="font-adbhashitha text-4xl text-center mb-12">Our Specialties</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{['Cinnamon Rolls', 'Sugar Cookies', 'Cakes'].map((item, index) => (
<div key={index} className="text-center p-6 bg-bakery-50 rounded-lg">
<h3 className="font-sans text-2xl mb-6">{item}</h3>
<div className="flex justify-center items-center mb-4">
<img
<h2 className="font-adbhashitha text-3xl md:text-4xl text-center text-bakery-900 mb-12">What people come in for</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 md:gap-8">
{['Cinnamon Rolls', 'Sugar Cookies', 'Cakes'].map((item) => (
<div
key={item}
className="group text-center bg-white rounded-3xl overflow-hidden shadow-sm hover:shadow-lg hover:-translate-y-1 transition duration-300"
>
<div className="overflow-hidden">
<Image
src={`/images/gallery/${item}.png`}
alt={item}
width={300}
height={200}
className="object-cover rounded-[20px] border border-black/20 shadow-lg"
width={600}
height={400}
sizes="(max-width: 640px) 100vw, (max-width: 768px) 50vw, 33vw"
className="w-full h-56 object-cover transition-transform duration-500 group-hover:scale-105"
/>
</div>
{/* <p className="text-bakery-700">Discover our selection of {item.toLowerCase()} prepared daily with love.</p> */}
<h3 className="font-adbhashitha text-xl md:text-2xl text-bakery-800 py-6 tracking-wide">{item}</h3>
</div>
))}
</div>
@@ -71,48 +79,53 @@ const HomePage = () => {
</section>
{/* About Us */}
<section className="py-16 bg-bakery-600 text-white">
<section className="py-16 md:py-24 bg-bakery-800 text-white">
<div className="container mx-auto px-4">
<div className="max-w-3xl mx-auto text-center">
<h2 className="font-adbhashitha text-4xl mb-8" style={{ letterSpacing: '0.01em' }}>About Us</h2>
<p className="text-lg mb-6">
Welcome to The Vine Bakery, where the warmth of home meets the heart of Princeville, IL. We are passionate about baking delicious, fresh goods that bring people together. Our cozy space invites you to gather with friends and family, sharing in the simple joy of homemade treats made with love and the finest ingredients. Come join us at The Vine and feel right at home.
<h2 className="font-adbhashitha text-3xl md:text-4xl mb-8" style={{ letterSpacing: '0.01em' }}>Our story</h2>
<p className="text-base md:text-lg text-bakery-100 mb-8 leading-relaxed">
Morissa Bennett opened The Vine in 2024. We bake in our own kitchen on Main Street:
cinnamon rolls, cookies, custom cakes, sandwiches, paninis, and coffee.
</p>
<Link href="/notre-histoire" className="text-white hover:text-bakery-700 font-semibold">
Learn More
<Link href="/history" className="text-white hover:text-bakery-200 font-semibold underline underline-offset-4">
Read our story
</Link>
</div>
</div>
</section>
{/* Hours & Contact */}
<section className="py-16 bg-white">
<section id="visit" className="py-16 md:py-24 bg-bakery-50 scroll-mt-24">
<div className="container mx-auto px-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
<div>
<h2 className="font-adbhashitha text-3xl mb-6">Our Hours</h2>
<ul className="space-y-3">
<li className="flex justify-between">
<span>Tuesday - Friday</span>
<span>7:00am - 2:00pm</span>
<h2 className="font-adbhashitha text-3xl md:text-4xl text-center text-bakery-900 mb-12">Visit us</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 max-w-4xl mx-auto">
<div className="bg-white rounded-3xl p-6 md:p-8">
<h2 className="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-6">Our hours</h2>
<ul className="space-y-3 text-bakery-800">
<li className="flex justify-between gap-4">
<span>Tuesday Friday</span>
<span className="font-medium whitespace-nowrap">7:00am 2:00pm</span>
</li>
<li className="flex justify-between">
<li className="flex justify-between gap-4">
<span>Saturday</span>
<span>7:00am - 12:00pm</span>
<span className="font-medium whitespace-nowrap">7:00am 12:00pm</span>
</li>
<li className="flex justify-between">
<span>Sunday - Monday</span>
<span>Closed</span>
<li className="flex justify-between gap-4">
<span>Sunday Monday</span>
<span className="font-medium">Closed</span>
</li>
</ul>
</div>
<div>
<h2 className="font-adbhashitha text-3xl mb-6">Contact</h2>
<address className="not-italic">
<p className="mb-2">215 E Main Street</p>
<p className="mb-2">61559 Princeville, IL</p>
<p className="mb-2">Phone: (309) 701-0660</p>
<p>Email: contact@itsthevine.com</p>
<div className="bg-white rounded-3xl p-6 md:p-8">
<h2 className="font-adbhashitha text-2xl md:text-3xl text-bakery-900 mb-6">Find us</h2>
<address className="not-italic space-y-3 text-bakery-800">
<p>215 E Main Street<br />Princeville, IL 61559</p>
<p>
<a href="tel:+13097010660" className="hover:text-bakery-600 underline underline-offset-4">(309) 701-0660</a>
</p>
<p className="break-words">
<a href="mailto:[email protected]" className="hover:text-bakery-600 underline underline-offset-4">contact@itsthevine.com</a>
</p>
</address>
</div>
</div>
-5
View File
@@ -1,5 +0,0 @@
const LoadingSpinner = () => (
<div className="animate-spin rounded-full h-12 w-12 border-4 border-bakery-600 border-t-transparent" />
);
export default LoadingSpinner;
+54 -20
View File
@@ -3,28 +3,62 @@ import Logo_R from 'public/images/resources/logo_R.svg';
import Logo_L from 'public/images/resources/logo_L.svg';
interface LogoProps {
logoColor: string;
/** Tailwind text-* class. The SVG marks fill with currentColor, so this colors the whole lockup. */
className?: string;
size?: 'sm' | 'lg';
/** The hero sits on the homepage, where a link back to "/" is pointless. */
linked?: boolean;
}
const Logo: React.FC<LogoProps> = ({ logoColor }) => {
// The lockup: branch · "The Vine" over "Coffeehouse + Bakery" · branch.
// Branch heights track the two-line wordmark so the marks read as part of it.
const SIZES = {
sm: {
branch: 'w-12 h-12 sm:w-14 sm:h-14',
name: 'text-xl sm:text-2xl md:text-3xl',
tag: 'text-[0.6rem] sm:text-xs tracking-[0.18em]',
gap: 'gap-1.5 sm:gap-2',
},
lg: {
branch: 'w-20 h-20 sm:w-28 sm:h-28',
name: 'text-4xl sm:text-5xl md:text-6xl',
tag: 'text-xs sm:text-base tracking-[0.2em]',
gap: 'gap-2 sm:gap-4',
},
};
const Logo: React.FC<LogoProps> = ({ className = 'text-bakery-700', size = 'sm', linked = true }) => {
const s = SIZES[size];
const inner = (
<>
<Logo_R className={`${s.branch} shrink-0`} />
<span className="flex flex-col items-center leading-none min-w-0">
<span className={`font-lejour ${s.name}`} style={{ letterSpacing: '0.01em' }}>
The Vine
</span>
<span className={`font-adbhashitha ${s.tag} uppercase mt-1.5 whitespace-nowrap`}>
Coffeehouse + Bakery
</span>
</span>
<Logo_L className={`${s.branch} shrink-0`} />
</>
);
const classes = `flex items-center ${s.gap} min-w-0 shrink ${className}`;
if (!linked) {
return (
<Link href="/" className="flex items-center">
<Logo_R fill={logoColor} width={60} height={60}/>
{/* <Image
src="/images/resources/logo_L.png"
alt="The Vine Coffeehouse & Bakery"
width={60}
height={60}
className="h-12 w-auto"
/> */}
<span className="ml-3 text-2xl text-bakery-800" style={{color: logoColor}}>
<span className="font-lejour" style={{ letterSpacing: '0.01em'}}>The Vine </span><span className="font-adbhashitha">Coffeehouse & Bakery</span>
</span>
<Logo_L fill={logoColor} width={60} height={60}/>
</Link>
<div className={`${classes} justify-center`} role="img" aria-label="The Vine Coffeehouse + Bakery">
{inner}
</div>
);
}
}
export default Logo;
return (
<Link href="/" className={classes} aria-label="The Vine Coffeehouse + Bakery, home">
{inner}
</Link>
);
};
export default Logo;
-188
View File
@@ -1,188 +0,0 @@
'use client'
import { useState, useEffect, useRef } from 'react';
interface LoyaltyCardFormProps {
isOpen: boolean;
onClose: () => void;
}
const LoyaltyCardForm = ({ isOpen, onClose }: LoyaltyCardFormProps) => {
const modalRef = useRef<HTMLDivElement>(null);
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
age: '',
address: '',
postalCode: '',
city: '',
email: '',
});
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
return () => {
document.body.style.overflow = 'unset';
};
}, [isOpen]);
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Form submitted:', formData);
setFormData({
firstName: '',
lastName: '',
age: '',
address: '',
postalCode: '',
city: '',
email: '',
});
onClose();
};
if (!isOpen) return null;
return (
<div className="fixed inset-0 z-50 overflow-y-auto bg-black/50 backdrop-blur-sm">
<div className="min-h-screen px-4 text-center">
{/* This element centers the modal */}
<span
className="inline-block h-screen align-middle"
aria-hidden="true"
>
&#8203;
</span>
{/* Modal */}
<div
ref={modalRef}
className="inline-block w-full max-w-md p-6 my-8 text-left align-middle bg-white rounded-lg shadow-xl transform transition-all"
onClick={(e) => e.stopPropagation()}
>
<div className="flex justify-between items-center mb-6">
<h2 className="font-sans text-2xl text-bakery-800">
Demande de Carte de Fidélité
</h2>
<button
onClick={onClose}
className="text-bakery-600 hover:text-bakery-800"
>
<svg className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-bakery-700 mb-1">
Prénom
</label>
<input
type="text"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-bakery-500"
value={formData.firstName}
onChange={(e) => setFormData({ ...formData, firstName: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium text-bakery-700 mb-1">
Nom
</label>
<input
type="text"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-bakery-500"
value={formData.lastName}
onChange={(e) => setFormData({ ...formData, lastName: e.target.value })}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-bakery-700 mb-1">
Âge
</label>
<input
type="number"
required
min="0"
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-bakery-500"
value={formData.age}
onChange={(e) => setFormData({ ...formData, age: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium text-bakery-700 mb-1">
Adresse
</label>
<input
type="text"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-bakery-500"
value={formData.address}
onChange={(e) => setFormData({ ...formData, address: e.target.value })}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-bakery-700 mb-1">
Code Postal
</label>
<input
type="text"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-bakery-500"
value={formData.postalCode}
onChange={(e) => setFormData({ ...formData, postalCode: e.target.value })}
/>
</div>
<div>
<label className="block text-sm font-medium text-bakery-700 mb-1">
Ville
</label>
<input
type="text"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-bakery-500"
value={formData.city}
onChange={(e) => setFormData({ ...formData, city: e.target.value })}
/>
</div>
</div>
<div>
<label className="block text-sm font-medium text-bakery-700 mb-1">
Email
</label>
<input
type="email"
required
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-bakery-500"
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
</div>
<button
type="submit"
className="w-full bg-bakery-600 text-white py-3 px-4 rounded-md hover:bg-bakery-700 transition duration-200 mt-6"
>
Demander ma carte
</button>
</form>
</div>
</div>
</div>
);
};
export default LoyaltyCardForm;
-59
View File
@@ -1,59 +0,0 @@
import { motion, AnimatePresence } from 'framer-motion';
import type { Product } from '@/types/product';
import AwesomeSlider from 'react-awesome-slider';
import "keen-slider/keen-slider.min.css"
interface ProductModalProps {
product: Product | null;
onClose: () => void;
}
const ProductModal = ({ product, onClose }: ProductModalProps) => {
if (!product) return null;
return (
<AnimatePresence>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="fixed inset-0 z-50 overflow-y-auto bg-black bg-opacity-50 backdrop-blur-sm flex items-center justify-center"
onClick={onClose}
>
<motion.div
initial={{ scale: 0.95, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
exit={{ scale: 0.95, opacity: 0 }}
className="inline-block w-full max-w-2xl p-6 my-8 text-left align-middle bg-white rounded-lg shadow-xl"
onClick={(e) => e.stopPropagation()}
>
<div className="relative h-80 mb-6">
<AwesomeSlider cssModule={AwesomeSlider}>
{product.images.map((imgUrl, index) => (
<div
key={index}
data-src={imgUrl}
className="w-full h-full bg-cover bg-center"
/>
))}
</AwesomeSlider>
</div>
<div className="flex justify-between items-start mb-4">
<h3 className="font-sans text-2xl text-bakery-800">{product.name}</h3>
{/* <span className="text-bakery-600 text-xl font-semibold">{product.price}</span> */}
</div>
<button
onClick={onClose}
className="mt-6 w-full bg-bakery-600 text-white py-3 rounded-md hover:bg-bakery-700 transition-colors"
>
Close
</button>
</motion.div>
</motion.div>
</AnimatePresence>
);
};
export default ProductModal;
+52 -91
View File
@@ -2,49 +2,27 @@
import { useState } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import { products, categories } from '@/data/products';
import LoadingSpinner from './LoadingSpinner';
import 'react-awesome-slider/dist/styles.css';
import AwesomeSlider from 'react-awesome-slider';
const ProductsPage = () => {
const [selectedCategory, setSelectedCategory] = useState('All');
// const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [isLoading, setIsLoading] = useState(false);
const filteredProducts = selectedCategory === 'All'
? products
: products.filter(product => product.category === selectedCategory);
const handleCategoryChange = (category: string) => {
setIsLoading(true);
setSelectedCategory(category);
// Simulate loading state
setTimeout(() => setIsLoading(false), 500);
};
return (
<div className="min-h-screen bg-bakery-50">
{/* Hero Section */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className="relative h-[40vh] bg-bakery-600"
>
<div className="absolute inset-0 bg-black/40 bg-cover bg-center" style={{ backgroundImage: 'url(/images/ressources/contact-bg.jpeg)', opacity: '0.6' }} />
<div className="relative container mx-auto px-4 h-[45vh] flex items-center justify-center text-center">
<div>
<h1 className="font-sans text-4xl md:text-5xl text-white mb-4">
Products
</h1>
<p className="text-white/90 text-lg max-w-2xl">
Explore our wide range of products crafted with the finest ingredients. Whether you are looking for something sweet or savory, we have something for everyone. Browse through our categories to find your perfect treat.
</p>
</div>
</div>
</motion.div>
{/* Page header */}
<header className="container mx-auto px-4 pt-14 pb-10 md:pt-20 md:pb-12 text-center">
<h1 className="font-adbhashitha text-4xl md:text-5xl text-bakery-900">
Our products
</h1>
</header>
{/* Products Section */}
<div className="container mx-auto px-4 py-16">
<div className="container mx-auto px-4 pb-16">
{/* Categories */}
<div className="flex flex-wrap justify-center gap-4 mb-12">
{categories.map((category) => (
@@ -52,11 +30,11 @@ const ProductsPage = () => {
key={category}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
onClick={() => handleCategoryChange(category)}
className={`px-6 py-2 rounded-full border-2 transition-colors ${
onClick={() => setSelectedCategory(category)}
className={`px-6 py-2 rounded-full border text-sm uppercase tracking-[0.12em] transition-colors ${
selectedCategory === category
? 'bg-bakery-600 text-white border-bakery-600'
: 'border-bakery-600 text-bakery-600 hover:bg-bakery-600 hover:text-white'
: 'bg-white border-bakery-300 text-bakery-700 hover:bg-bakery-100'
}`}
>
{category}
@@ -65,67 +43,50 @@ const ProductsPage = () => {
</div>
{/* Products Grid */}
{isLoading ? (
<div className="flex justify-center items-center h-96">
<LoadingSpinner />
</div>
) : (
<motion.div
layout
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
>
<AnimatePresence>
{filteredProducts.map((product) => (
<motion.div
key={product.id}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="group bg-white rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300"
>
{/* Image Container */}
<div
className="relative w-full overflow-hidden cursor-pointer">
<AwesomeSlider
bullets={false}
style={{ aspectRatio: '1 / 1' }}
organicArrows={product.images.length > 1}
customContent={true}
buttonContentLeft={product.images.length > 1 ? <span className="text-white text-2xl">{'<'}</span> : null}
buttonContentRight={product.images.length > 1 ? <span className="text-white text-2xl">{'>'}</span> : null}
>
{product.images.map((image, index) => (
<motion.div
layout
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
>
<AnimatePresence>
{filteredProducts.map((product) => (
<motion.div
key={product.id}
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="group bg-white rounded-3xl overflow-hidden shadow-sm hover:shadow-lg hover:-translate-y-1 transition duration-300"
>
{/* Image Container */}
<div className="relative w-full overflow-hidden">
<AwesomeSlider
bullets={false}
style={{ aspectRatio: '1 / 1' }}
organicArrows={product.images.length > 1}
customContent={true}
buttonContentLeft={product.images.length > 1 ? <span className="text-white text-2xl">{'<'}</span> : null}
buttonContentRight={product.images.length > 1 ? <span className="text-white text-2xl">{'>'}</span> : null}
>
{product.images.map((image, index) => (
<div key={index} data-src={image} />
))}
</AwesomeSlider>
</div>
))}
</AwesomeSlider>
</div>
{/* Content */}
<div className="p-6">
<div className="flex justify-between items-start mb-2">
<h3 className="font-sans text-xl text-bakery-800">
{product.name}
</h3>
</div>
<div className="flex justify-between items-center">
<span className="text-sm text-bakery-500">
{product.category}
</span>
</div>
</div>
</motion.div>
))}
</AnimatePresence>
</motion.div>
)}
{/* Content */}
<div className="p-6 text-center">
<h3 className="font-adbhashitha text-xl text-bakery-900 mb-2 tracking-wide">
{product.name}
</h3>
<span className="text-xs uppercase tracking-[0.15em] text-bakery-600">
{product.category}
</span>
</div>
</motion.div>
))}
</AnimatePresence>
</motion.div>
</div>
{/* Product Modal */}
{/* <ProductModal
product={selectedProduct}
onClose={() => setSelectedProduct(null)}
/> */}
</div>
);
};