Initial commit - Route Commerce platform
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, Suspense, lazy } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||
import { getBrandSettingsPublic } from "@/actions/brand-settings";
|
||||
|
||||
// Lazy load heavy sections
|
||||
const MissionSection = lazy(() => import("@/components/about/IndianRiverMissionSection"));
|
||||
const CitrusSection = lazy(() => import("@/components/about/IndianRiverCitrusSection"));
|
||||
const TeamSection = lazy(() => import("@/components/about/IndianRiverTeamSection"));
|
||||
const ValuesSection = lazy(() => import("@/components/about/IndianRiverValuesSection"));
|
||||
const ContactSection = lazy(() => import("@/components/about/IndianRiverContactSection"));
|
||||
const CTASection = lazy(() => import("@/components/about/IndianRiverCTASection"));
|
||||
|
||||
export default function IndianRiverAboutPage() {
|
||||
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||
const [logoUrlDark, setLogoUrlDark] = useState<string | null>(null);
|
||||
const [showWholesaleLink, setShowWholesaleLink] = useState(true);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [customFooterText, setCustomFooterText] = useState<string | null>(null);
|
||||
const [contactEmail, setContactEmail] = useState<string | null>(null);
|
||||
const [contactPhone, setContactPhone] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const settingsResult = await getBrandSettingsPublic("indian-river-direct");
|
||||
if (settingsResult.success && settingsResult.settings) {
|
||||
const s = settingsResult.settings;
|
||||
setLogoUrl(s.logo_url ?? null);
|
||||
setLogoUrlDark(s.logo_url_dark ?? null);
|
||||
setCustomFooterText(s.custom_footer_text ?? null);
|
||||
setContactEmail(s.email ?? null);
|
||||
setContactPhone(s.phone ?? null);
|
||||
setShowWholesaleLink(s.show_wholesale_link ?? true);
|
||||
}
|
||||
try {
|
||||
const { getCurrentAdminUser } = await import("@/actions/admin-user");
|
||||
setIsAdmin(!!await getCurrentAdminUser());
|
||||
} catch { /* not logged in */ }
|
||||
}
|
||||
load();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<StorefrontHeader
|
||||
brandName="Indian River Direct"
|
||||
brandSlug="indian-river-direct"
|
||||
logoUrl={logoUrl}
|
||||
logoUrlDark={logoUrlDark}
|
||||
showWholesaleLink={showWholesaleLink}
|
||||
isAdmin={isAdmin}
|
||||
brandAccent="blue"
|
||||
/>
|
||||
|
||||
<main>
|
||||
{/* Hero Section */}
|
||||
<section className="relative bg-gradient-to-br from-blue-600 via-blue-700 to-blue-800 overflow-hidden">
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute inset-0 opacity-20">
|
||||
<div className="absolute top-0 left-1/4 w-96 h-96 bg-blue-400/30 rounded-full blur-3xl" />
|
||||
<div className="absolute bottom-0 right-1/4 w-80 h-80 bg-blue-300/20 rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
<div className="relative mx-auto max-w-6xl px-6 py-24 md:py-36">
|
||||
<div className="grid lg:grid-cols-2 gap-12 items-center">
|
||||
{/* Text content */}
|
||||
<div>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="text-[11px] font-bold uppercase tracking-[0.3em] text-white/80 mb-4"
|
||||
>
|
||||
Since 1985
|
||||
</motion.p>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.15 }}
|
||||
className="text-5xl md:text-6xl lg:text-7xl font-black tracking-tight text-white leading-[1] mb-6"
|
||||
>
|
||||
Our Story
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
className="text-xl md:text-2xl text-white/90 leading-relaxed"
|
||||
>
|
||||
A Passion for All Things Fruit — From Our Grove to Your Table
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
initial={{ scaleX: 0 }}
|
||||
animate={{ scaleX: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.5 }}
|
||||
className="mt-8 h-1 w-16 bg-blue-400 rounded-full origin-left"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Brand imagery */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
className="flex justify-center lg:justify-end"
|
||||
>
|
||||
<div className="relative">
|
||||
<div className="absolute -inset-6 bg-blue-900/30 rounded-full blur-2xl" />
|
||||
{logoUrlDark ? (
|
||||
<img
|
||||
src={logoUrlDark}
|
||||
alt="Indian River Direct"
|
||||
className="relative w-56 md:w-72 h-auto object-contain"
|
||||
/>
|
||||
) : (
|
||||
<div className="relative w-32 h-32 md:w-40 md:h-40 rounded-full bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center shadow-2xl">
|
||||
<span className="text-3xl md:text-4xl font-black text-white">IRD</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom transition */}
|
||||
<div className="absolute -bottom-px left-0 right-0 h-3 bg-stone-50" />
|
||||
</section>
|
||||
|
||||
{/* Mission Section */}
|
||||
<Suspense fallback={<LoadingFallback bg="white" />}>
|
||||
<MissionSection />
|
||||
</Suspense>
|
||||
|
||||
{/* Citrus Varieties */}
|
||||
<Suspense fallback={<LoadingFallback bg="bg-stone-50" />}>
|
||||
<CitrusSection />
|
||||
</Suspense>
|
||||
|
||||
{/* Values */}
|
||||
<Suspense fallback={<LoadingFallback bg="white" />}>
|
||||
<ValuesSection />
|
||||
</Suspense>
|
||||
|
||||
{/* Team Section */}
|
||||
<Suspense fallback={<LoadingFallback bg="bg-stone-50" />}>
|
||||
<TeamSection />
|
||||
</Suspense>
|
||||
|
||||
{/* Contact Section */}
|
||||
<Suspense fallback={<LoadingFallback bg="bg-stone-950" />}>
|
||||
<ContactSection phone={contactPhone} email={contactEmail} />
|
||||
</Suspense>
|
||||
|
||||
{/* CTA Section */}
|
||||
<Suspense fallback={<LoadingFallback bg="bg-blue-600" />}>
|
||||
<CTASection />
|
||||
</Suspense>
|
||||
</main>
|
||||
|
||||
<StorefrontFooter
|
||||
brandName="Indian River Direct"
|
||||
brandSlug="indian-river-direct"
|
||||
logoUrl={logoUrl}
|
||||
logoUrlDark={logoUrlDark}
|
||||
customFooterText={customFooterText}
|
||||
contactEmail={contactEmail}
|
||||
contactPhone={contactPhone}
|
||||
isAdmin={isAdmin}
|
||||
brandAccent="blue"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Simple loading fallback - no more skeleton spinners
|
||||
function LoadingFallback({ bg }: { bg: string }) {
|
||||
return <div className={`py-24 md:py-32 ${bg}`} />;
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
type BrandSettings = {
|
||||
invoice_business_name: string | null;
|
||||
invoice_business_address: string | null;
|
||||
invoice_business_phone: string | null;
|
||||
invoice_business_email: string | null;
|
||||
invoice_business_website: string | null;
|
||||
};
|
||||
|
||||
type FormState = {
|
||||
name: string;
|
||||
email: string;
|
||||
topic: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export default function IndianRiverContactPage() {
|
||||
const [brandSettings, setBrandSettings] = useState<BrandSettings | null>(null);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [form, setForm] = useState<FormState>({ name: "", email: "", topic: "general", message: "" });
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [customFooterText, setCustomFooterText] = useState<string | null>(null);
|
||||
const [contactEmail, setContactEmail] = useState<string | null>(null);
|
||||
const [contactPhone, setContactPhone] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
supabase.from("brands").select("id").eq("slug", "indian-river-direct").single().then(({ data: brand }) => {
|
||||
if (!brand?.id) return;
|
||||
supabase.from("wholesale_settings")
|
||||
.select("invoice_business_name, invoice_business_address, invoice_business_phone, invoice_business_email, invoice_business_website")
|
||||
.eq("brand_id", brand.id).single().then(({ data }) => setBrandSettings(data ?? null));
|
||||
supabase.from("brand_settings").select("logo_url, custom_footer_text, email, phone")
|
||||
.eq("brand_id", brand.id).single().then(({ data: s }) => {
|
||||
if (s) { setLogoUrl(s.logo_url ?? null); setCustomFooterText(s.custom_footer_text ?? null); setContactEmail(s.email ?? null); setContactPhone(s.phone ?? null); }
|
||||
});
|
||||
try { import("@/actions/admin-user").then(({ getCurrentAdminUser }) => { getCurrentAdminUser().then((u: unknown) => setIsAdmin(!!u)); }); } catch { /* not logged in */ }
|
||||
});
|
||||
}, []);
|
||||
|
||||
function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setIsSubmitting(true);
|
||||
setTimeout(() => {
|
||||
setSubmitted(true);
|
||||
setIsSubmitting(false);
|
||||
}, 600);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<StorefrontHeader brandName="Indian River Direct" brandSlug="indian-river-direct" logoUrl={logoUrl} showWholesaleLink={true} isAdmin={isAdmin} brandAccent="blue" />
|
||||
|
||||
<main className="px-6 py-16 md:py-20">
|
||||
<LayoutContainer>
|
||||
<div className="max-w-5xl mx-auto">
|
||||
|
||||
{/* Page header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.7, ease: [0.25, 0.1, 0.25, 1] }}
|
||||
className="text-center mb-16 p-10 rounded-3xl bg-white border-2 border-stone-200 shadow-xl"
|
||||
>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-blue-600 mb-4">Get in Touch</p>
|
||||
<h1 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-tight">Contact Us</h1>
|
||||
<p className="mt-5 text-lg text-stone-600 max-w-xl mx-auto leading-relaxed">
|
||||
Questions about citrus, your order, or becoming a wholesale partner? We'd love to hear from you.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Contact info cards */}
|
||||
<div className="grid gap-6 md:grid-cols-3 mb-16">
|
||||
{[
|
||||
{
|
||||
label: "Address",
|
||||
value: brandSettings?.invoice_business_address ?? "Indian River Region, Florida",
|
||||
icon: <path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />,
|
||||
iconPath: <path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
|
||||
},
|
||||
{
|
||||
label: "Phone",
|
||||
value: contactPhone ?? "772-971-4484",
|
||||
icon: <path strokeLinecap="round" strokeLinejoin="round" d="M2.25 6.75c0 8.284 6.716 15 15 15h2.25a2.25 2.25 0 002.25-2.25v-1.372c0-.516-.351-.966-.852-1.091l-4.423-1.106c-.184-.046-.379-.041-.545.114L18 10.48a2.25 2.25 0 00-.545-.114l-4.423 1.106c-.5.119-.852.575-.852 1.091v1.372a2.25 2.25 0 01-2.25 2.25h-2.25a15 15 0 01-15-15z" />,
|
||||
iconPath: null
|
||||
},
|
||||
{
|
||||
label: "Email",
|
||||
value: brandSettings?.invoice_business_email ?? "Info@indianriverdirect.com",
|
||||
icon: <path strokeLinecap="round" strokeLinejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 01-2.25 2.25h-15a2.25 2.25 0 01-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0019.5 4.5h-15a2.25 2.25 0 00-2.25 2.25m19.5 0v.243a2.25 2.25 0 01-1.07 1.916l-7.5 4.615a2.25 2.25 0 01-2.36 0L3.32 8.91a2.25 2.25 0 01-1.07-1.916V6.75" />,
|
||||
iconPath: null
|
||||
},
|
||||
].map(({ label, value, icon, iconPath }, index) => (
|
||||
<motion.div
|
||||
key={label}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.6, delay: index * 0.1 }}
|
||||
className="rounded-3xl bg-white border-2 border-stone-200 p-8 shadow-xl text-center hover:shadow-2xl hover:-translate-y-1 transition-all duration-300"
|
||||
>
|
||||
<div className="inline-flex h-14 w-14 items-center justify-center rounded-2xl bg-blue-100 mb-5 shadow-md">
|
||||
<svg className="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.75}>
|
||||
{icon}
|
||||
{iconPath}
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-stone-950 mb-3">{label}</h3>
|
||||
<p className="text-base text-stone-600 leading-relaxed">{value}</p>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Contact form */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.7, ease: [0.25, 0.1, 0.25, 1], delay: 0.2 }}
|
||||
className="rounded-3xl bg-white border-2 border-stone-200 p-10 shadow-xl"
|
||||
>
|
||||
<div className="mb-8">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-blue-600 mb-3">Send a Message</p>
|
||||
<h2 className="text-3xl font-black text-stone-950 tracking-tight leading-tight">
|
||||
Get in Touch
|
||||
</h2>
|
||||
<div className="mt-4 h-px w-12 bg-blue-600" />
|
||||
</div>
|
||||
{submitted ? (
|
||||
<div className="rounded-2xl bg-blue-50 border-2 border-blue-100 p-10 text-center shadow-md">
|
||||
<div className="inline-flex h-14 w-14 items-center justify-center rounded-full bg-blue-100 mb-4 shadow-md">
|
||||
<svg className="h-7 w-7 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-xl font-bold text-stone-950">Message received.</p>
|
||||
<p className="mt-2 text-base text-stone-500">We will be in touch within 1-2 business days.</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSubmitted(false);
|
||||
setForm({ name: "", email: "", topic: "general", message: "" });
|
||||
}}
|
||||
className="mt-5 text-sm font-semibold text-blue-700 underline hover:text-blue-900"
|
||||
>
|
||||
Send another message
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-stone-700 mb-2">Your Name</label>
|
||||
<input
|
||||
required
|
||||
value={form.name}
|
||||
onChange={(e) => setForm({ ...form, name: e.target.value })}
|
||||
className="w-full rounded-xl border-2 border-stone-200 bg-white px-5 py-4 text-base text-stone-900 placeholder:text-stone-400 outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-400/20 transition-colors"
|
||||
placeholder="Jane Smith"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-stone-700 mb-2">Email</label>
|
||||
<input
|
||||
required
|
||||
type="email"
|
||||
value={form.email}
|
||||
onChange={(e) => setForm({ ...form, email: e.target.value })}
|
||||
className="w-full rounded-xl border-2 border-stone-200 bg-white px-5 py-4 text-base text-stone-900 placeholder:text-stone-400 outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-400/20 transition-colors"
|
||||
placeholder="jane@example.com"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-stone-700 mb-2">Topic</label>
|
||||
<select
|
||||
value={form.topic}
|
||||
onChange={(e) => setForm({ ...form, topic: e.target.value })}
|
||||
className="w-full rounded-xl border-2 border-stone-200 bg-white px-5 py-4 text-base text-stone-900 outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-400/20 transition-colors appearance-none"
|
||||
>
|
||||
<option value="general">General Question</option>
|
||||
<option value="orders">Orders & Pickup</option>
|
||||
<option value="wholesale">Wholesale Inquiry</option>
|
||||
<option value="product">Product Question</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-semibold text-stone-700 mb-2">Message</label>
|
||||
<textarea
|
||||
required
|
||||
rows={5}
|
||||
value={form.message}
|
||||
onChange={(e) => setForm({ ...form, message: e.target.value })}
|
||||
className="w-full rounded-xl border-2 border-stone-200 bg-white px-5 py-4 text-base text-stone-900 placeholder:text-stone-400 outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-400/20 transition-colors resize-none"
|
||||
placeholder="How can we help you?"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="rounded-xl bg-blue-600 hover:bg-blue-500 px-8 py-4 text-base font-bold text-white hover:shadow-lg hover:shadow-blue-600/25 transition-all disabled:opacity-60 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isSubmitting ? "Sending..." : "Send Message"}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
</motion.div>
|
||||
|
||||
{/* Business hours */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.7, ease: [0.25, 0.1, 0.25, 1], delay: 0.3 }}
|
||||
className="mt-10 rounded-3xl bg-white border-2 border-stone-200 p-10 shadow-xl"
|
||||
>
|
||||
<div className="mb-6">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-blue-600 mb-3">Seasonal Hours</p>
|
||||
<h2 className="text-2xl font-black text-stone-950 tracking-tight leading-tight">
|
||||
When to Reach Us
|
||||
</h2>
|
||||
<div className="mt-4 h-px w-12 bg-blue-600" />
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="flex items-center justify-between py-4 px-6 rounded-2xl bg-stone-50 border border-stone-200">
|
||||
<p className="font-semibold text-stone-700">Monday – Friday</p>
|
||||
<p className="text-stone-600 font-medium">8:00 AM – 5:00 PM</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between py-4 px-6 rounded-2xl bg-stone-50 border border-stone-200">
|
||||
<p className="font-semibold text-stone-700">Saturday – Sunday</p>
|
||||
<p className="text-stone-600 font-medium">By Appointment</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-5 text-sm text-stone-400 leading-relaxed">
|
||||
During the off-season, response times may be longer. For urgent matters, please call the office.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Regional info */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true }}
|
||||
transition={{ duration: 0.7, ease: [0.25, 0.1, 0.25, 1], delay: 0.4 }}
|
||||
className="mt-10 rounded-3xl bg-white border-2 border-stone-200 p-10 shadow-xl"
|
||||
>
|
||||
<div className="mb-4">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-blue-600 mb-3">Delivery Area</p>
|
||||
<h2 className="text-2xl font-black text-stone-950 tracking-tight leading-tight">Regional Delivery</h2>
|
||||
<div className="mt-4 h-px w-12 bg-blue-600" />
|
||||
</div>
|
||||
<p className="text-stone-600 leading-relaxed text-base">
|
||||
We deliver fresh citrus directly to pickup stops across the Southeast and Midwest. Enter your ZIP code on our homepage to find stops near you, or browse our upcoming schedule below.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</main>
|
||||
|
||||
<StorefrontFooter brandName="Indian River Direct" brandSlug="indian-river-direct" logoUrl={logoUrl} customFooterText={customFooterText} contactEmail={contactEmail} contactPhone={contactPhone} isAdmin={isAdmin} brandAccent="blue" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
export default function ErrorPage({
|
||||
error,
|
||||
reset,
|
||||
}: {
|
||||
error: Error & { digest?: string };
|
||||
reset: () => void;
|
||||
}) {
|
||||
return (
|
||||
<main className="min-h-screen bg-red-50 px-6 py-12">
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<h1 className="text-3xl font-bold text-red-900">Page Error</h1>
|
||||
<p className="mt-4 text-red-700">{error.message}</p>
|
||||
{error.digest && (
|
||||
<p className="mt-2 text-sm text-red-500">Digest: {error.digest}</p>
|
||||
)}
|
||||
<button
|
||||
onClick={reset}
|
||||
className="mt-6 rounded-xl bg-red-600 px-4 py-2 text-white"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useMemo } from "react";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
|
||||
type FAQCategory = {
|
||||
category: string;
|
||||
items: FAQItem[];
|
||||
};
|
||||
|
||||
type FAQItem = { question: string; answer: string };
|
||||
|
||||
function buildFaqCategories(phone: string): FAQCategory[] {
|
||||
return [
|
||||
{
|
||||
category: "Ordering & Pickup",
|
||||
items: [
|
||||
{ question: "How does preordering work?", answer: "Select your preferred pickup stop from our schedule, then browse our available products. Add items to your cart and select your stop at checkout. Preordering helps us know how much to bring to each stop — and ensures you get what you want." },
|
||||
{ question: "When do I need to order by?", answer: "We recommend ordering at least 2 days before your selected pickup date. Orders placed after the cutoff may still be accepted if space allows — contact us to check." },
|
||||
{ question: "Can I change my pickup stop after ordering?", answer: "Yes — as long as your order hasn't been marked 'fulfilled,' contact us and we can move your order to a different stop. Log into the portal or call us to make the change." },
|
||||
{ question: "What if I need to cancel my order?", answer: `Contact us at least 24 hours before your pickup date for a full refund. Cancellations within 24 hours may be subject to a 50% fee. Email Info@indianriverdirect.com or call ${phone}.` },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Product & Season",
|
||||
items: [
|
||||
{ question: "What citrus is available when?", answer: "Our season runs year-round with different fruits at different times: Navels (Dec–Mar), Ruby Red Grapefruit (Jan–Apr), Honeybells (Jan–Feb), Super Sweet Tangerines (Jan–Mar), Temples (Mar–Apr), Peaches (Jun–Aug), Blueberries (Jun–Jul). Check the homepage or contact us for this season's exact availability." },
|
||||
{ question: "Do you offer organic citrus?", answer: "Our growers use integrated pest management and sustainable farming practices. Some items may be certified organic — contact us for current availability and certifications." },
|
||||
{ question: "How do I store my citrus after pickup?", answer: "Citrus should be stored in the refrigerator to maintain freshness. Most varieties keep 1–2 weeks when refrigerated. Do not freeze citrus unless you plan to use it in cooking." },
|
||||
{ question: "Why are some items by preorder only?", answer: "Preordering helps us manage inventory and ensures we bring enough product to each stop. Preordered items are prioritized for fulfillment — if you show up without preordering, some items may sell out." },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Shipping & Delivery",
|
||||
items: [
|
||||
{ question: "Can I have my order shipped instead of picked up?", answer: `Currently we offer pickup-only ordering through this site. For bulk or wholesale shipping inquiries, please contact us directly at ${phone}.` },
|
||||
{ question: "Do you deliver to my area?", answer: "We operate pickup stops across the Southeast and Midwest — Ohio, Indiana, Kentucky, West Virginia, Wisconsin, and Illinois. Enter your ZIP code on our homepage to find stops near you. We do not offer residential delivery." },
|
||||
{ question: "What if no stops are near me?", answer: "If you don't see any stops near your location, we're not currently serving your area. Contact us and we can note your interest for future route expansion. Wholesale customers may also qualify for direct delivery." },
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "Wholesale",
|
||||
items: [
|
||||
{ question: "How do I apply for a wholesale account?", answer: "Visit /wholesale/register to apply. We review applications and typically respond within 1–2 business days. Wholesale accounts are available to retailers, restaurants, farm stands, and other businesses." },
|
||||
{ question: "Is there a minimum order for wholesale?", answer: "Minimum wholesale order is $150 per order. We also require orders to meet a per-item minimum in some categories. Contact us for details." },
|
||||
{ question: "Do you offer recurring standing orders?", answer: "Yes — wholesale accounts can set up recurring orders for weekly or biweekly delivery to their stop. Contact your wholesale representative to set up a standing order schedule." },
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const DEFAULT_PHONE = "772-971-4484";
|
||||
|
||||
function FAQAccordion({ items, searchQuery }: { items: FAQItem[]; searchQuery: string }) {
|
||||
const [open, setOpen] = useState<string | null>(null);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!searchQuery.trim()) return items;
|
||||
const q = searchQuery.toLowerCase();
|
||||
return items.filter(
|
||||
(item) =>
|
||||
item.question.toLowerCase().includes(q) ||
|
||||
item.answer.toLowerCase().includes(q)
|
||||
);
|
||||
}, [items, searchQuery]);
|
||||
|
||||
if (filtered.length === 0) {
|
||||
return (
|
||||
<div className="rounded-2xl bg-white border-2 border-stone-200 p-8 text-center shadow-lg">
|
||||
<p className="text-stone-500 text-base">No results for "{searchQuery}"</p>
|
||||
<p className="mt-1 text-stone-400 text-sm">Try a different term or browse all categories below.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{filtered.map((item) => {
|
||||
const isOpen = open === item.question;
|
||||
return (
|
||||
<div
|
||||
key={item.question}
|
||||
className="rounded-2xl bg-white border-2 border-stone-200 overflow-hidden transition-all duration-300 hover:border-blue-200 hover:shadow-lg"
|
||||
>
|
||||
<button
|
||||
onClick={() => setOpen(isOpen ? null : item.question)}
|
||||
className="flex w-full items-center justify-between px-6 py-5 text-left group"
|
||||
aria-expanded={isOpen}
|
||||
>
|
||||
<span className="font-semibold text-stone-950 pr-4 text-base leading-snug group-hover:text-blue-600 transition-colors">
|
||||
{item.question}
|
||||
</span>
|
||||
<motion.span
|
||||
animate={{ rotate: isOpen ? 180 : 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center transition-all duration-300 ${
|
||||
isOpen
|
||||
? "bg-blue-600 text-white shadow-lg"
|
||||
: "bg-stone-100 text-stone-500 group-hover:bg-blue-100 group-hover:text-blue-600"
|
||||
}`}
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2.5}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</motion.span>
|
||||
</button>
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-300 ease-out ${
|
||||
isOpen ? "max-h-48 opacity-100" : "max-h-0 opacity-0"
|
||||
}`}
|
||||
>
|
||||
<div className="px-6 pb-6 pt-1 border-t border-stone-100">
|
||||
<p className="text-sm text-stone-600 leading-relaxed">{item.answer}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function IndianRiverFAQPage() {
|
||||
const [faqCategories, setFaqCategories] = useState(buildFaqCategories(DEFAULT_PHONE));
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
supabase.from("brand_settings").select("phone").eq("brand_id",
|
||||
"b1cb7a96-d82b-40b1-80b1-d6dd26c56e28"
|
||||
).single().then(({ data }) => {
|
||||
if (data?.phone) setFaqCategories(buildFaqCategories(data.phone));
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<StorefrontHeader brandName="Indian River Direct" brandSlug="indian-river-direct" brandAccent="blue" />
|
||||
|
||||
<main className="py-16 md:py-20">
|
||||
<LayoutContainer>
|
||||
<div className="max-w-3xl mx-auto">
|
||||
{/* Page header */}
|
||||
<div className="text-center mb-14 p-10 rounded-3xl bg-white border-2 border-stone-200 shadow-xl">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-blue-600 mb-4">Questions</p>
|
||||
<h1 className="text-5xl md:text-6xl font-black tracking-tight text-stone-950 leading-tight">
|
||||
FAQ
|
||||
</h1>
|
||||
<p className="mt-5 text-lg text-stone-600 leading-relaxed">
|
||||
Everything you need to know about ordering, pickup, and our seasonal fruits.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Search input */}
|
||||
<div className="mb-12 relative">
|
||||
<div className="absolute inset-y-0 left-5 flex items-center pointer-events-none">
|
||||
<svg className="h-5 w-5 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 5.196a7.5 7.5 0 0010.607 10.607z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search questions..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full rounded-2xl border-2 border-stone-200 bg-white px-12 py-4 text-base text-stone-900 placeholder:text-stone-400 outline-none focus:border-blue-400 focus:ring-2 focus:ring-blue-400/20 transition-colors shadow-lg"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
onClick={() => setSearchQuery("")}
|
||||
className="absolute inset-y-0 right-5 flex items-center text-stone-400 hover:text-stone-600 transition-colors"
|
||||
>
|
||||
<svg className="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* FAQ categories */}
|
||||
<div className="space-y-14">
|
||||
{faqCategories.map((cat) => (
|
||||
<div key={cat.category}>
|
||||
<h2 className="text-xs font-bold uppercase tracking-[0.2em] text-blue-600 mb-5">
|
||||
{cat.category}
|
||||
</h2>
|
||||
<FAQAccordion items={cat.items} searchQuery={searchQuery} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Still have questions CTA */}
|
||||
<div className="mt-16 rounded-3xl bg-gradient-to-br from-blue-600 to-blue-700 p-10 text-center shadow-2xl relative overflow-hidden">
|
||||
{/* Decorative elements */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-0 right-0 w-40 h-40 bg-white rounded-full -translate-y-1/2 translate-x-1/4" />
|
||||
<div className="absolute bottom-0 left-0 w-32 h-32 bg-white rounded-full translate-y-1/2 -translate-x-1/4" />
|
||||
</div>
|
||||
<div className="relative">
|
||||
<p className="text-2xl font-black text-white tracking-tight">Still have questions?</p>
|
||||
<p className="mt-2 text-blue-100 text-sm">We're happy to help — reach out anytime.</p>
|
||||
<Link
|
||||
href="/indian-river-direct/contact"
|
||||
className="mt-8 inline-flex items-center gap-2.5 rounded-full bg-white px-8 py-4 font-bold text-blue-700 hover:bg-blue-50 transition-all text-sm tracking-wider shadow-lg hover:shadow-xl"
|
||||
>
|
||||
Contact Us
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</main>
|
||||
|
||||
<StorefrontFooter brandName="Indian River Direct" brandSlug="indian-river-direct" brandAccent="blue" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Indian River Direct | Peach & Citrus Truckload",
|
||||
description: "Fresh peaches and citrus from our Florida groves to truckload sales in your neighborhood. Family-owned since 1985. Pre-order now for 2026 season.",
|
||||
};
|
||||
|
||||
export default function IndianRiverLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen relative">
|
||||
{/* Glass morphism gradient background */}
|
||||
<div className="fixed inset-0 bg-gradient-to-br from-blue-100/40 via-white/60 to-blue-50/30" />
|
||||
|
||||
{/* Decorative glass orbs */}
|
||||
<div className="fixed inset-0 pointer-events-none overflow-hidden">
|
||||
<div className="absolute -top-40 -right-40 w-96 h-96 bg-gradient-to-br from-blue-200/30 to-transparent rounded-full blur-3xl" />
|
||||
<div className="absolute top-1/3 -left-20 w-72 h-72 bg-gradient-to-br from-blue-100/25 to-transparent rounded-full blur-2xl" />
|
||||
<div className="absolute bottom-0 right-1/4 w-80 h-80 bg-gradient-to-br from-blue-50/40 to-transparent rounded-full blur-3xl" />
|
||||
</div>
|
||||
|
||||
{/* Content wrapper */}
|
||||
<div className="relative">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main className="min-h-screen bg-stone-50 px-6 py-12">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="animate-pulse space-y-6">
|
||||
<div className="h-8 w-56 rounded bg-slate-200" />
|
||||
<div className="h-12 w-96 rounded bg-slate-200" />
|
||||
<div className="h-4 w-full max-w-xl rounded bg-slate-200" />
|
||||
<div className="mt-8 rounded-2xl bg-white p-6 shadow-sm" />
|
||||
<div className="mt-10 grid gap-6 md:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<div key={i} className="h-48 rounded-2xl bg-slate-200" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||
import WhyIndianRiverDirect from "@/components/storefront/WhyIndianRiverDirect";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { getBrandSettingsPublic } from "@/actions/brand-settings";
|
||||
|
||||
type Brand = { id: string; name: string; slug: string };
|
||||
type Stop = { id: string; city: string; state: string; date: string; time: string; location: string; slug: string; brand_id: string };
|
||||
type Product = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
price: number;
|
||||
type: string;
|
||||
image_url: string | null;
|
||||
brand_id: string;
|
||||
is_taxable?: boolean;
|
||||
pickup_type?: "scheduled_stop" | "shed";
|
||||
seasonal?: boolean;
|
||||
season_start?: string;
|
||||
season_end?: string;
|
||||
preorder?: boolean;
|
||||
price_tba?: boolean;
|
||||
pickup_only?: boolean;
|
||||
};
|
||||
|
||||
const TESTIMONIALS = [
|
||||
{ name: "Linda Hurlbut", text: "I finally got my grapefruit from you today. OMG thank you for bringing your fruit to us. It is amazing. We will see you next month & tell our friends.", rating: 5 },
|
||||
{ name: "Phil Myers", text: "I just wanted to comment on the citrus I received. They were absolutely the best I have got from you over the past few years!", rating: 5 },
|
||||
{ name: "Bill Prue", text: "I would just like to say how pleased we are with your Orrie Tangerines! Very good indeed!!", rating: 5 },
|
||||
];
|
||||
|
||||
const SCHEDULE_PDFS = [
|
||||
{ region: "OH-IN-KY-WV", url: "https://cdn.shopify.com/s/files/1/0506/2908/3294/files/2026_OH-IN-KY_Peaches_Draft_Website.pdf" },
|
||||
{ region: "WI-IL", url: "https://cdn.shopify.com/s/files/1/0506/2908/3294/files/2026_WI-IL_Schedule_Draft_Website.pdf" },
|
||||
];
|
||||
|
||||
// Scroll-triggered fade-in wrapper
|
||||
function FadeInSection({
|
||||
children,
|
||||
className = "",
|
||||
delay = 0
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
delay?: number;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isInView = useInView(ref, { once: true, margin: "-60px" });
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
ref={ref}
|
||||
initial={{ opacity: 0, y: 32 }}
|
||||
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 32 }}
|
||||
transition={{ duration: 0.7, delay, ease: [0.22, 0.61, 0.36, 1] }}
|
||||
className={className}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function IndianRiverDirectPage() {
|
||||
const [brand, setBrand] = useState<Brand | null>(null);
|
||||
const [stops, setStops] = useState<Stop[]>([]);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [logoUrl, setLogoUrl] = useState<string | null>(null);
|
||||
const [logoUrlDark, setLogoUrlDark] = useState<string | null>(null);
|
||||
const [showSchedulePdf, setShowSchedulePdf] = useState(true);
|
||||
const [showWholesaleLink, setShowWholesaleLink] = useState(true);
|
||||
const [heroTagline, setHeroTagline] = useState<string | null>(null);
|
||||
const [isAdmin, setIsAdmin] = useState(false);
|
||||
const [customFooterText, setCustomFooterText] = useState<string | null>(null);
|
||||
const [contactEmail, setContactEmail] = useState<string | null>(null);
|
||||
const [contactPhone, setContactPhone] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const slug = "indian-river-direct";
|
||||
const [brandResult, settingsResult] = await Promise.all([
|
||||
supabase.from("brands").select("*").eq("slug", slug).single(),
|
||||
getBrandSettingsPublic(slug),
|
||||
]);
|
||||
|
||||
const brandData = brandResult.data;
|
||||
setBrand(brandData);
|
||||
|
||||
if (settingsResult.success && settingsResult.settings) {
|
||||
const s = settingsResult.settings;
|
||||
setLogoUrl(s.logo_url ?? null);
|
||||
setLogoUrlDark(s.logo_url_dark ?? null);
|
||||
setHeroTagline(s.hero_tagline ?? null);
|
||||
setCustomFooterText(s.custom_footer_text ?? null);
|
||||
setContactEmail(s.email ?? null);
|
||||
setContactPhone(s.phone ?? null);
|
||||
setShowSchedulePdf(s.show_schedule_pdf ?? true);
|
||||
setShowWholesaleLink(s.show_wholesale_link ?? true);
|
||||
}
|
||||
try {
|
||||
const { getCurrentAdminUser } = await import("@/actions/admin-user");
|
||||
setIsAdmin(!!await getCurrentAdminUser());
|
||||
} catch { /* not logged in */ }
|
||||
|
||||
if (brandData?.id) {
|
||||
const [{ data: stopsData }, { data: productsData }] = await Promise.all([
|
||||
supabase.from("stops").select("*").eq("brand_id", brandData.id).eq("active", true),
|
||||
supabase.from("products").select("*").eq("brand_id", brandData.id).eq("active", true),
|
||||
]);
|
||||
setStops(stopsData ?? []);
|
||||
setProducts(productsData ?? []);
|
||||
}
|
||||
}
|
||||
load();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white/50 backdrop-blur-xl">
|
||||
<StorefrontHeader
|
||||
brandName={brand?.name ?? "Indian River Direct"}
|
||||
brandSlug="indian-river-direct"
|
||||
logoUrl={logoUrl}
|
||||
logoUrlDark={logoUrlDark}
|
||||
showWholesaleLink={showWholesaleLink}
|
||||
isAdmin={isAdmin}
|
||||
brandAccent="blue"
|
||||
/>
|
||||
|
||||
<main>
|
||||
{/* ── Hero — Light Fresh Background ── */}
|
||||
<section className="relative bg-gradient-to-b from-blue-50 via-white to-stone-50 overflow-hidden min-h-[640px] flex items-center">
|
||||
{/* Subtle decorative elements */}
|
||||
<div className="absolute top-0 right-0 w-96 h-96 bg-blue-100/40 rounded-full -translate-y-1/2 translate-x-1/4 blur-3xl" />
|
||||
<div className="absolute bottom-0 left-0 w-80 h-80 bg-blue-50/60 rounded-full translate-y-1/2 -translate-x-1/4 blur-3xl" />
|
||||
|
||||
<LayoutContainer>
|
||||
<div className="relative z-10 mx-auto max-w-4xl text-center pt-20 pb-16">
|
||||
{/* Eyebrow */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
className="text-xs font-bold uppercase tracking-[0.3em] text-blue-600 mb-5"
|
||||
>
|
||||
Since 1985
|
||||
</motion.p>
|
||||
|
||||
{/* Headline */}
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 24 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
className="text-5xl md:text-6xl lg:text-7xl font-black tracking-tight text-stone-950 leading-[1.05]"
|
||||
>
|
||||
Fresh From<br />The Grove
|
||||
</motion.h1>
|
||||
|
||||
{/* Subheading */}
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.7, delay: 0.35 }}
|
||||
className="mt-8 text-xl md:text-2xl text-stone-600 leading-relaxed max-w-2xl mx-auto"
|
||||
>
|
||||
{heroTagline ?? "Family-owned and operated. Bringing the finest peaches, pecans, and seasonal citrus from our Florida groves directly to your neighborhood."}
|
||||
</motion.p>
|
||||
|
||||
{/* CTA Buttons */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.5 }}
|
||||
className="mt-12 flex gap-4 justify-center flex-wrap"
|
||||
>
|
||||
<Link
|
||||
href="/indian-river-direct#stops"
|
||||
className="rounded-2xl bg-blue-600 px-8 py-4 font-bold text-white hover:bg-blue-500 active:bg-blue-700 hover:-translate-y-0.5 hover:shadow-xl hover:shadow-blue-900/20 transition-all text-sm tracking-wider"
|
||||
>
|
||||
Find a Stop
|
||||
</Link>
|
||||
<Link
|
||||
href="/indian-river-direct#products"
|
||||
className="rounded-2xl bg-white px-8 py-4 font-bold text-blue-600 hover:bg-blue-50 active:bg-blue-100 hover:-translate-y-0.5 hover:shadow-xl shadow-sm transition-all text-sm tracking-wider border border-blue-200"
|
||||
>
|
||||
Shop Products
|
||||
</Link>
|
||||
</motion.div>
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
|
||||
{/* Scroll indicator */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.2, duration: 0.6 }}
|
||||
className="absolute bottom-8 left-1/2 -translate-x-1/2"
|
||||
>
|
||||
<motion.div
|
||||
animate={{ y: [0, 8, 0] }}
|
||||
transition={{ duration: 1.8, repeat: Infinity, ease: "easeInOut" }}
|
||||
className="flex flex-col items-center gap-2"
|
||||
>
|
||||
<span className="text-[10px] uppercase tracking-widest text-stone-400">Scroll</span>
|
||||
<svg className="w-5 h-5 text-stone-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19 14l-7 7m0 0l-7-7m7 7V3" />
|
||||
</svg>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</section>
|
||||
|
||||
{/* ── Why Indian River Direct Section ── */}
|
||||
<WhyIndianRiverDirect />
|
||||
|
||||
{/* ── Pickup-Only Notice ── */}
|
||||
<FadeInSection className="py-16 bg-white relative">
|
||||
<LayoutContainer>
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<div className="rounded-3xl bg-gradient-to-br from-blue-50/80 to-stone-50/80 backdrop-blur-sm border border-blue-100/50 p-8 shadow-xl shadow-blue-200/30">
|
||||
<div className="flex items-center justify-center gap-5">
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-12 h-12 rounded-2xl bg-blue-600 flex items-center justify-center shadow-lg shadow-blue-600/30">
|
||||
<svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-stone-950">Pickup Only — Direct from the Truck</h3>
|
||||
<p className="text-stone-600 text-sm mt-1">All products picked up at our scheduled truck stops across Ohio, Indiana, Kentucky, West Virginia, Wisconsin, and Illinois.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</FadeInSection>
|
||||
|
||||
{/* ── Schedule Downloads ── */}
|
||||
{showSchedulePdf && (
|
||||
<FadeInSection className="py-20 bg-gradient-to-b from-blue-50/50 to-stone-50/50 relative">
|
||||
<LayoutContainer>
|
||||
<div className="mb-12">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-blue-600 mb-4">2026 Season</p>
|
||||
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-stone-950 leading-[1.05]">Pickup Schedule</h2>
|
||||
<div className="mt-4 h-px w-12 bg-blue-600" />
|
||||
<p className="mt-4 text-stone-600">Download our schedule to find stops in your area.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 max-w-2xl">
|
||||
{SCHEDULE_PDFS.map((pdf) => (
|
||||
<a
|
||||
key={pdf.region}
|
||||
href={pdf.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded-2xl bg-white border-2 border-stone-200 p-6 flex items-center gap-4 group shadow-lg hover:shadow-xl hover:border-blue-200 hover:-translate-y-1 transition-all duration-300"
|
||||
>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="w-14 h-14 rounded-2xl bg-gradient-to-br from-blue-600 to-blue-700 flex items-center justify-center shadow-lg shadow-blue-600/30">
|
||||
<svg className="w-6 h-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-bold text-stone-950 text-lg">{pdf.region}</h3>
|
||||
<p className="text-stone-500 text-sm">2026 Season Schedule</p>
|
||||
</div>
|
||||
<svg className="h-5 w-5 text-blue-500 group-hover:text-blue-700 group-hover:translate-x-1 transition-all" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</FadeInSection>
|
||||
)}
|
||||
|
||||
{/* ── Upcoming Stops ── */}
|
||||
<FadeInSection className="py-20 bg-white relative">
|
||||
<LayoutContainer>
|
||||
<div className="mb-12">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-blue-600 mb-4">Family Farms</p>
|
||||
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-stone-950 leading-[1.05]">Upcoming Stops</h2>
|
||||
<div className="mt-4 h-px w-12 bg-blue-600" />
|
||||
<p className="mt-4 text-stone-600">Find a pickup location near you.</p>
|
||||
</div>
|
||||
|
||||
{stops.length === 0 ? (
|
||||
<div className="rounded-3xl bg-stone-50 border-2 border-stone-200 p-12 text-center">
|
||||
<p className="text-stone-500">No upcoming stops scheduled. Check back soon for the 2026 season!</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{stops.slice(0, 6).map((stop) => (
|
||||
<div key={stop.id} className="rounded-2xl bg-gradient-to-br from-blue-50 to-white border-2 border-blue-100 p-5 shadow-lg hover:shadow-xl hover:border-blue-200 hover:-translate-y-1 transition-all duration-300">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="font-bold text-stone-950 text-lg">{stop.city}, {stop.state}</h3>
|
||||
<p className="text-stone-500 text-sm">{stop.location}</p>
|
||||
</div>
|
||||
<span className="shrink-0 rounded-full bg-blue-600 px-3 py-1 text-xs font-bold text-white shadow-sm">
|
||||
Pickup
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-stone-600">
|
||||
<p className="font-semibold">{stop.date}</p>
|
||||
<p>{stop.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{stops.length > 6 && (
|
||||
<div className="mt-8 text-center">
|
||||
<Link
|
||||
href="/indian-river-direct/stops"
|
||||
className="inline-flex items-center gap-2 rounded-full bg-blue-600 px-6 py-3 text-sm font-semibold text-white hover:bg-blue-500 transition-colors"
|
||||
>
|
||||
View all stops
|
||||
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</LayoutContainer>
|
||||
</FadeInSection>
|
||||
|
||||
{/* ── Products Section ── */}
|
||||
<FadeInSection className="py-20 bg-stone-100 relative">
|
||||
<LayoutContainer>
|
||||
<div className="mb-12">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-blue-600 mb-4">Fresh Products</p>
|
||||
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-stone-950 leading-[1.05]">From Our Grove to You</h2>
|
||||
<div className="mt-4 h-px w-12 bg-blue-600" />
|
||||
<p className="mt-4 max-w-2xl text-lg text-stone-600 leading-relaxed">
|
||||
Hand-picked at peak ripeness, delivered fresh from Titan Farms (South Carolina peaches) and Ellis Brothers (Georgia pecans) directly to your community.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{products.length === 0 ? (
|
||||
<div className="rounded-3xl bg-white border-2 border-stone-200 p-12 text-center shadow-lg">
|
||||
<p className="text-stone-500">Products coming soon for the 2026 season!</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{products.map((product) => (
|
||||
<div key={product.id} className="rounded-2xl bg-white border-2 border-stone-200 overflow-hidden shadow-xl hover:shadow-2xl hover:-translate-y-2 hover:border-blue-200 transition-all duration-300">
|
||||
{/* Product Image */}
|
||||
<div className="aspect-[4/3] bg-gradient-to-br from-blue-600 to-blue-700 flex items-center justify-center relative">
|
||||
{product.image_url ? (
|
||||
<img src={product.image_url} alt={product.name} className="w-full h-full object-cover" />
|
||||
) : (
|
||||
<div className="text-6xl opacity-30">🍑</div>
|
||||
)}
|
||||
{/* Badges */}
|
||||
<div className="absolute top-3 left-3 flex flex-wrap gap-2">
|
||||
{product.preorder && (
|
||||
<span className="rounded-full bg-white text-blue-700 px-3 py-1 text-xs font-bold shadow-lg">
|
||||
Pre-Order
|
||||
</span>
|
||||
)}
|
||||
{product.seasonal && (
|
||||
<span className="rounded-full bg-stone-900/80 backdrop-blur-sm px-3 py-1 text-xs font-medium text-white">
|
||||
{product.season_start} - {product.season_end}
|
||||
</span>
|
||||
)}
|
||||
{product.pickup_only && (
|
||||
<span className="rounded-full bg-blue-600 text-white px-3 py-1 text-xs font-medium shadow-md">
|
||||
Pickup Only
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Product Info */}
|
||||
<div className="p-6">
|
||||
<h3 className="text-lg font-bold text-stone-950 mb-2">{product.name}</h3>
|
||||
<p className="text-stone-600 text-sm leading-relaxed line-clamp-2 mb-4">
|
||||
{product.description}
|
||||
</p>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-2xl font-bold text-stone-950">
|
||||
{product.price_tba ? "Price TBA" : `$${product.price.toFixed(2)}`}
|
||||
</span>
|
||||
{product.price > 0 && (
|
||||
<button className="rounded-xl bg-blue-600 hover:bg-blue-500 px-5 py-2.5 text-sm font-bold text-white transition-all shadow-lg hover:shadow-xl">
|
||||
{product.preorder ? "Pre-Order" : "Add to Cart"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</LayoutContainer>
|
||||
</FadeInSection>
|
||||
|
||||
{/* ── Testimonials ── */}
|
||||
<FadeInSection className="py-20 bg-gradient-to-b from-blue-50 to-blue-100/50 relative">
|
||||
<LayoutContainer>
|
||||
<div className="text-center mb-12">
|
||||
<div className="flex items-center justify-center gap-3 mb-3">
|
||||
<span className="h-px w-12 bg-gradient-to-r from-transparent to-blue-600" />
|
||||
<div className="flex">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<svg key={i} className="w-5 h-5 text-amber-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
<span className="h-px w-12 bg-gradient-to-l from-transparent to-blue-600" />
|
||||
</div>
|
||||
<h2 className="text-3xl md:text-4xl font-black text-stone-950 tracking-tight">What Our Customers Say</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{TESTIMONIALS.map((t, i) => (
|
||||
<div key={i} className="rounded-2xl bg-white border-2 border-stone-200 p-6 shadow-xl hover:shadow-2xl hover:-translate-y-1 transition-all duration-300">
|
||||
<div className="flex mb-4">
|
||||
{[...Array(t.rating)].map((_, j) => (
|
||||
<svg key={j} className="w-5 h-5 text-amber-500" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
<blockquote className="text-stone-600 text-sm leading-relaxed mb-4">
|
||||
"{t.text}"
|
||||
</blockquote>
|
||||
<p className="text-stone-950 font-bold text-sm">— {t.name}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</FadeInSection>
|
||||
|
||||
{/* ── Newsletter ── */}
|
||||
<FadeInSection className="py-20 bg-stone-100 relative">
|
||||
<LayoutContainer>
|
||||
<div className="mx-auto max-w-2xl text-center">
|
||||
<div className="rounded-3xl bg-gradient-to-br from-blue-600 to-blue-700 p-10 shadow-2xl shadow-blue-900/30 relative overflow-hidden">
|
||||
{/* Decorative pattern */}
|
||||
<div className="absolute inset-0 opacity-10">
|
||||
<div className="absolute top-0 right-0 w-40 h-40 bg-white rounded-full -translate-y-1/2 translate-x-1/4" />
|
||||
<div className="absolute bottom-0 left-0 w-32 h-32 bg-white rounded-full translate-y-1/2 -translate-x-1/4" />
|
||||
</div>
|
||||
<div className="relative">
|
||||
<h2 className="text-2xl md:text-3xl font-black text-white">Stay in the Loop</h2>
|
||||
<p className="mt-2 text-blue-100">Get the annual schedules and tour updates delivered to your inbox.</p>
|
||||
<form className="flex gap-3 max-w-md mx-auto mt-6" onSubmit={(e) => e.preventDefault()}>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
className="flex-1 rounded-xl border-2 border-white/30 bg-white/90 backdrop-blur-sm px-4 py-3 text-stone-900 placeholder:text-stone-400 outline-none focus:border-white focus:ring-2 focus:ring-white/50 transition-all"
|
||||
/>
|
||||
<button className="rounded-xl bg-white text-blue-700 hover:bg-blue-50 px-6 py-3 font-bold transition-all shadow-lg hover:shadow-xl whitespace-nowrap">
|
||||
Subscribe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</FadeInSection>
|
||||
|
||||
{/* ── CTA ── */}
|
||||
<section className="py-20 bg-gradient-to-br from-blue-600 via-blue-700 to-blue-800 relative overflow-hidden">
|
||||
{/* Decorative glow */}
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_50%,rgba(255,255,255,0.1),transparent_50%)]" />
|
||||
|
||||
<LayoutContainer>
|
||||
<div className="relative text-center">
|
||||
<h2 className="text-2xl md:text-3xl font-black text-white">Ready to order?</h2>
|
||||
<p className="mt-2 text-blue-100">Find a stop near you or browse our seasonal products.</p>
|
||||
<div className="mt-8 flex gap-4 justify-center flex-wrap">
|
||||
<Link href="/indian-river-direct#stops" className="rounded-2xl bg-white px-8 py-4 font-bold text-blue-600 hover:bg-blue-50 active:bg-blue-100 hover:-translate-y-0.5 hover:shadow-lg shadow-sm transition-all text-sm tracking-wider">
|
||||
Find a Stop
|
||||
</Link>
|
||||
<Link href="/indian-river-direct/about" className="rounded-2xl bg-blue-700/50 backdrop-blur-sm border border-white/30 px-8 py-4 font-bold text-white hover:bg-blue-600/80 hover:-translate-y-0.5 hover:shadow-lg shadow-sm transition-all text-sm tracking-wider">
|
||||
Our Story
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<StorefrontFooter
|
||||
brandName={brand?.name ?? "Indian River Direct"}
|
||||
brandSlug="indian-river-direct"
|
||||
logoUrl={logoUrl}
|
||||
logoUrlDark={logoUrlDark}
|
||||
customFooterText={customFooterText}
|
||||
contactEmail={contactEmail}
|
||||
contactPhone={contactPhone}
|
||||
isAdmin={isAdmin}
|
||||
brandAccent="blue"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
export default function Loading() {
|
||||
return (
|
||||
<main className="min-h-screen bg-blue-50 px-6 py-12">
|
||||
<div className="mx-auto max-w-5xl">
|
||||
<div className="animate-pulse space-y-6">
|
||||
<div className="h-6 w-40 rounded bg-slate-200" />
|
||||
<div className="h-12 w-80 rounded bg-slate-200" />
|
||||
<div className="mt-8 rounded-2xl bg-white p-8 shadow-sm" />
|
||||
<div className="mt-10 grid gap-6 md:grid-cols-3">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<div key={i} className="h-64 rounded-2xl bg-slate-200" />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { motion } from "framer-motion";
|
||||
import ProductCard from "@/components/storefront/ProductCard";
|
||||
import StopSetEffect from "@/components/storefront/StopSetEffect";
|
||||
import StorefrontHeader from "@/components/storefront/StorefrontHeader";
|
||||
import StorefrontFooter from "@/components/storefront/StorefrontFooter";
|
||||
import LayoutContainer from "@/components/layout/LayoutContainer";
|
||||
import { supabase } from "@/lib/supabase";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
|
||||
type Product = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
price: number;
|
||||
type: string;
|
||||
image_url: string | null;
|
||||
brand_id: string;
|
||||
is_taxable?: boolean;
|
||||
pickup_type?: "scheduled_stop" | "shed";
|
||||
};
|
||||
|
||||
export default function StopPage() {
|
||||
const params = useParams();
|
||||
const slug = params.slug as string;
|
||||
|
||||
const [stop, setStop] = useState<{
|
||||
id: string;
|
||||
city: string;
|
||||
state: string;
|
||||
date: string;
|
||||
time: string;
|
||||
location: string;
|
||||
brand_id: string;
|
||||
} | null>(null);
|
||||
const [products, setProducts] = useState<Product[]>([]);
|
||||
const [brandSlug, setBrandSlug] = useState("indian-river-direct");
|
||||
const [brandAccent, setBrandAccent] = useState<"green" | "orange" | "blue">("blue");
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const { data: stopData } = await supabase
|
||||
.from("stops")
|
||||
.select("*")
|
||||
.eq("slug", slug)
|
||||
.eq("active", true)
|
||||
.single();
|
||||
|
||||
if (!stopData) return;
|
||||
|
||||
setStop(stopData);
|
||||
const isIndian = slug.includes("indian");
|
||||
setBrandSlug(isIndian ? "indian-river-direct" : "tuxedo");
|
||||
setBrandAccent(isIndian ? "blue" : "green");
|
||||
|
||||
const { data: productsData } = await supabase
|
||||
.from("products")
|
||||
.select("*")
|
||||
.eq("brand_id", stopData.brand_id)
|
||||
.eq("active", true);
|
||||
setProducts(productsData ?? []);
|
||||
}
|
||||
load();
|
||||
}, [slug]);
|
||||
|
||||
if (!stop) {
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50">
|
||||
<StorefrontHeader brandName="Indian River Direct" brandSlug="indian-river-direct" brandAccent="blue" />
|
||||
<main className="px-6 py-20">
|
||||
<LayoutContainer>
|
||||
<div className="max-w-5xl mx-auto">
|
||||
<div className="animate-pulse space-y-4">
|
||||
<div className="h-4 w-32 bg-stone-200 rounded" />
|
||||
<div className="h-16 w-64 bg-stone-200 rounded" />
|
||||
</div>
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isBlue = brandAccent === "blue";
|
||||
const brandLabel = isBlue ? "Indian River Direct" : "Tuxedo Corn";
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-stone-50/80 backdrop-blur-xl">
|
||||
<StorefrontHeader
|
||||
brandName={brandLabel}
|
||||
brandSlug={brandSlug}
|
||||
brandAccent={brandAccent}
|
||||
/>
|
||||
|
||||
<main className="px-6 py-16 md:py-20">
|
||||
<LayoutContainer>
|
||||
<div className="max-w-5xl mx-auto">
|
||||
|
||||
{/* Back navigation */}
|
||||
<div className="mb-10 flex items-center gap-2">
|
||||
<Link
|
||||
href={`/${brandSlug}#stops`}
|
||||
className="flex items-center gap-2 text-sm font-medium text-stone-500 hover:text-stone-800 transition-colors group"
|
||||
>
|
||||
<svg
|
||||
className="h-4 w-4 transition-transform duration-200 group-hover:-translate-x-1"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
strokeWidth={2}
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
All Stops
|
||||
</Link>
|
||||
<span className="text-stone-300">/</span>
|
||||
<span className="text-sm text-stone-700 font-medium">{stop.city}, {stop.state}</span>
|
||||
</div>
|
||||
|
||||
{/* Stop header with animation */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6 }}
|
||||
className="backdrop-blur-sm bg-white/60 border border-white/50 rounded-3xl p-8 mb-12"
|
||||
>
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-blue-600 mb-4">
|
||||
{isBlue ? "Indian River Direct" : "Tuxedo Corn"}
|
||||
</p>
|
||||
<h1 className="text-5xl md:text-7xl font-black tracking-tight text-stone-950 leading-[1.0]">
|
||||
{stop.city},<br className="hidden md:block" /> {stop.state}
|
||||
</h1>
|
||||
<p className="mt-5 max-w-xl text-lg text-stone-500 leading-relaxed">
|
||||
Order fresh Florida citrus for pickup at this stop.
|
||||
</p>
|
||||
<div className="mt-6 h-px w-12 bg-blue-600" />
|
||||
</motion.div>
|
||||
|
||||
{/* Stop info */}
|
||||
<div className="backdrop-blur-md bg-white/80 border border-white/50 rounded-3xl p-8 mb-14 shadow-xl shadow-stone-200/50">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{/* Date */}
|
||||
<div className="flex items-start gap-4 py-4 px-5 rounded-2xl bg-stone-50">
|
||||
<div className={`flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center ${isBlue ? "bg-blue-50" : "bg-emerald-50"}`}>
|
||||
<svg className={`h-5 w-5 ${isBlue ? "text-blue-500" : "text-emerald-600"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-1">Date</p>
|
||||
<p className="font-bold text-stone-950">{formatDate(stop.date)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Time */}
|
||||
<div className="flex items-start gap-4 py-4 px-5 rounded-2xl bg-stone-50">
|
||||
<div className={`flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center ${isBlue ? "bg-blue-50" : "bg-emerald-50"}`}>
|
||||
<svg className={`h-5 w-5 ${isBlue ? "text-blue-500" : "text-emerald-600"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-1">Time</p>
|
||||
<p className="font-bold text-stone-950">{stop.time}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/* Location */}
|
||||
<div className="flex items-start gap-4 py-4 px-5 rounded-2xl bg-stone-50">
|
||||
<div className={`flex-shrink-0 w-10 h-10 rounded-xl flex items-center justify-center ${isBlue ? "bg-blue-50" : "bg-emerald-50"}`}>
|
||||
<svg className={`h-5 w-5 ${isBlue ? "text-blue-500" : "text-emerald-600"}`} fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 10.5a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-stone-400 mb-1">Location</p>
|
||||
<p className="font-bold text-stone-950 leading-tight">{stop.location}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Available Products — editorial header */}
|
||||
<section>
|
||||
<div className="mb-10">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-widest text-blue-600 mb-4">Farm-Direct</p>
|
||||
<h2 className="text-4xl md:text-5xl font-black tracking-tight text-stone-950 leading-tight">
|
||||
Available Products
|
||||
</h2>
|
||||
<div className="mt-5 h-px w-12 bg-blue-600" />
|
||||
<p className="mt-5 text-base text-stone-500 leading-relaxed">
|
||||
Preorder for pickup — add items below and we will have them ready at the stop.
|
||||
</p>
|
||||
</div>
|
||||
{products.length === 0 ? (
|
||||
<div className="backdrop-blur-sm bg-white/70 border border-white/50 rounded-3xl p-12 text-center shadow-lg">
|
||||
<p className="text-stone-500">No products available for this stop.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
{products.map((product) => (
|
||||
<ProductCard
|
||||
key={product.id}
|
||||
id={product.id}
|
||||
name={product.name}
|
||||
description={product.description}
|
||||
price={`$${product.price}`}
|
||||
type={product.type}
|
||||
imageUrl={product.image_url}
|
||||
brandSlug={brandSlug}
|
||||
brandName={brandLabel}
|
||||
brandId={product.brand_id}
|
||||
brandAccent={brandAccent}
|
||||
is_taxable={product.is_taxable}
|
||||
pickup_type={product.pickup_type}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</LayoutContainer>
|
||||
</main>
|
||||
|
||||
<StorefrontFooter
|
||||
brandName={brandLabel}
|
||||
brandSlug={brandSlug}
|
||||
brandAccent={brandAccent}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user