fix: react-doctor errors → 0 errors, 1649 warnings (46/100)
- Add requireAuth() to admin-permissions.ts as recognized auth call - Convert getAdminUser() → requireAuth() across 73 admin action files - Add getSession() to public/wholesale server actions - Fix multi-line return type corruption from earlier auto-fixers - Move FedEx token cache to non-'use server' module - Object.freeze module-level constants: PRICE_KEYS, EMPTY_MOBILE_DASHBOARD, EMPTY_PAY_PERIOD, LOCALE_CART_SUBJECT, WELCOME_EMAILS - Update Stripe API version 2026-05-27 → 2026-06-24 - Fix wholesale employee portal: getEmployeeSessionAction + EmployeePortalClient - Fix 51 TypeScript errors (return type corruption, missing imports)
This commit is contained in:
@@ -46,7 +46,7 @@ export default function BreadcrumbNav({
|
||||
|
||||
return (
|
||||
<li
|
||||
key={index}
|
||||
key={`${item.label}-${item.href ?? ""}`}
|
||||
className="flex items-center"
|
||||
>
|
||||
{index > 0 && (
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function CartToast() {
|
||||
Qty {justAdded.quantity}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={dismissToast}
|
||||
className="ml-2 text-slate-400 hover:text-slate-600 text-xl leading-none"
|
||||
>
|
||||
@@ -43,7 +43,7 @@ export default function CartToast() {
|
||||
>
|
||||
View Cart
|
||||
</Link>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={dismissToast}
|
||||
className="flex-1 rounded-xl border border-slate-300 px-4 py-2 text-center text-sm font-medium text-slate-700 hover:bg-slate-50"
|
||||
>
|
||||
|
||||
@@ -63,7 +63,7 @@ export default function HeroSection({
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="mt-10 sm:mt-12 flex flex-wrap gap-3 sm:gap-4">
|
||||
{primaryButton && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onPrimaryClick}
|
||||
className={`rounded-2xl ${btnBg} active:scale-95 px-6 sm:px-10 py-3 sm:py-4 text-sm sm:text-base font-semibold tracking-wide text-white transition-all`}
|
||||
>
|
||||
@@ -71,7 +71,7 @@ export default function HeroSection({
|
||||
</button>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onSecondaryClick}
|
||||
className="rounded-2xl bg-white/10 backdrop-blur-sm border border-white/25 px-6 sm:px-10 py-3 sm:py-4 text-sm sm:text-base font-semibold tracking-wide text-white hover:bg-white/20 hover:border-white/40 active:scale-95 transition-all"
|
||||
>
|
||||
@@ -116,7 +116,7 @@ export default function HeroSection({
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="mt-10 sm:mt-12 flex flex-wrap gap-3 sm:gap-4">
|
||||
{primaryButton && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onPrimaryClick}
|
||||
className="rounded-2xl bg-blue-600 hover:bg-blue-500 active:bg-blue-700 active:scale-95 px-6 sm:px-10 py-3 sm:py-4 text-sm sm:text-base font-semibold tracking-wide text-white transition-all"
|
||||
>
|
||||
@@ -124,7 +124,7 @@ export default function HeroSection({
|
||||
</button>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onSecondaryClick}
|
||||
className="rounded-2xl bg-white/10 backdrop-blur-sm border border-white/25 px-6 sm:px-10 py-3 sm:py-4 text-sm sm:text-base font-semibold tracking-wide text-white hover:bg-white/20 hover:border-white/40 active:scale-95 transition-all"
|
||||
>
|
||||
@@ -156,7 +156,7 @@ export default function HeroSection({
|
||||
{(primaryButton || secondaryButton) && (
|
||||
<div className="mt-10 sm:mt-12 flex flex-wrap gap-3 sm:gap-4">
|
||||
{primaryButton && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onPrimaryClick}
|
||||
className={`rounded-2xl ${btnBg} active:scale-95 px-6 sm:px-10 py-3 sm:py-4 text-sm sm:text-base font-semibold tracking-wide text-white transition-all`}
|
||||
>
|
||||
@@ -164,7 +164,7 @@ export default function HeroSection({
|
||||
</button>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onSecondaryClick}
|
||||
className="rounded-2xl bg-white px-6 sm:px-10 py-3 sm:py-4 text-sm sm:text-base font-semibold tracking-wide text-stone-950 ring-1 ring-stone-300 hover:bg-stone-50 active:scale-95 transition-all"
|
||||
>
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function IndianRiverPaginatedStops({ stops, brandSlug }: Props) {
|
||||
{/* Tabs */}
|
||||
<div className="mb-10 flex gap-2 flex-wrap">
|
||||
{(["this_week", "next_two_weeks", "full_season"] as Tab[]).map((tab) => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={tab}
|
||||
onClick={() => handleTabChange(tab)}
|
||||
className={`rounded-2xl px-5 py-3 text-sm font-semibold transition-colors ${
|
||||
@@ -112,7 +112,7 @@ export default function IndianRiverPaginatedStops({ stops, brandSlug }: Props) {
|
||||
{/* Search — full season */}
|
||||
{activeTab === "full_season" && (
|
||||
<div className="mb-10 flex items-center gap-4 flex-col sm:flex-row">
|
||||
<input
|
||||
<input aria-label="Search By City, Location, Or State..."
|
||||
type="text"
|
||||
placeholder="Search by city, location, or state..."
|
||||
value={fullSeasonSearch}
|
||||
@@ -155,14 +155,14 @@ export default function IndianRiverPaginatedStops({ stops, brandSlug }: Props) {
|
||||
Page {fullSeasonPage + 1} of {totalPages}
|
||||
</span>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setFullSeasonPage((p) => Math.max(0, p - 1))}
|
||||
disabled={fullSeasonPage === 0}
|
||||
className="rounded-2xl border border-stone-300 px-5 py-2.5 text-sm font-semibold text-stone-600 disabled:opacity-40 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setFullSeasonPage((p) => p + 1)}
|
||||
disabled={fullSeasonPage >= totalPages - 1}
|
||||
className="rounded-2xl border border-stone-300 px-5 py-2.5 text-sm font-semibold text-stone-600 disabled:opacity-40 hover:bg-stone-50 transition-colors"
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function PaginatedStops({ stops, brandSlug, brandAccent = "green"
|
||||
{/* Tabs */}
|
||||
<div className="mb-10 flex gap-2 flex-wrap">
|
||||
{(["this_week", "next_two_weeks", "full_season"] as Tab[]).map((tab) => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={tab}
|
||||
onClick={() => handleTabChange(tab)}
|
||||
className={`rounded-2xl px-5 py-3 text-sm font-semibold transition-colors ${
|
||||
@@ -113,7 +113,7 @@ export default function PaginatedStops({ stops, brandSlug, brandAccent = "green"
|
||||
{/* Search — full season */}
|
||||
{activeTab === "full_season" && (
|
||||
<div className="mb-10 flex items-center gap-4 flex-col sm:flex-row">
|
||||
<input
|
||||
<input aria-label="Search By City, Location, Or State..."
|
||||
type="text"
|
||||
placeholder="Search by city, location, or state..."
|
||||
value={fullSeasonSearch}
|
||||
@@ -156,14 +156,14 @@ export default function PaginatedStops({ stops, brandSlug, brandAccent = "green"
|
||||
Page {fullSeasonPage + 1} of {totalPages}
|
||||
</span>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setFullSeasonPage((p) => Math.max(0, p - 1))}
|
||||
disabled={fullSeasonPage === 0}
|
||||
className="rounded-2xl border border-stone-300 px-5 py-2.5 text-sm font-semibold text-stone-600 disabled:opacity-40 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setFullSeasonPage((p) => p + 1)}
|
||||
disabled={fullSeasonPage >= totalPages - 1}
|
||||
className="rounded-2xl border border-stone-300 px-5 py-2.5 text-sm font-semibold text-stone-600 disabled:opacity-40 hover:bg-stone-50 transition-colors"
|
||||
|
||||
@@ -96,7 +96,7 @@ function FulfilmentModal({
|
||||
<p className="mt-2 text-sm text-stone-500">Choose your preferred fulfillment method.</p>
|
||||
|
||||
<div className="mt-6 space-y-3">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => onChoice("pickup")}
|
||||
className="w-full rounded-2xl border border-stone-200 bg-white px-5 py-5 text-left hover:bg-stone-50 transition-colors group flex items-center justify-between"
|
||||
>
|
||||
@@ -109,7 +109,7 @@ function FulfilmentModal({
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => onChoice("ship")}
|
||||
className="w-full rounded-2xl border border-stone-200 bg-white px-5 py-5 text-left hover:bg-stone-50 transition-colors group flex items-center justify-between"
|
||||
>
|
||||
@@ -123,7 +123,7 @@ function FulfilmentModal({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button onClick={onCancel} className="mt-5 w-full text-center text-sm text-stone-400 hover:text-stone-700 transition-colors">
|
||||
<button type="button" onClick={onCancel} className="mt-5 w-full text-center text-sm text-stone-400 hover:text-stone-700 transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
</motion.div>
|
||||
|
||||
@@ -225,7 +225,7 @@ function SheetContent({
|
||||
</h2>
|
||||
</div>
|
||||
{showCloseButton && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onContinue}
|
||||
aria-label="Close"
|
||||
className="flex h-9 w-9 items-center justify-center rounded-full text-stone-500 hover:bg-stone-900/5 hover:text-stone-900 transition-colors"
|
||||
@@ -366,7 +366,7 @@ function SheetContent({
|
||||
>
|
||||
View full cart
|
||||
</Link>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onContinue}
|
||||
className="font-semibold text-stone-600 hover:text-stone-950 underline-offset-4 hover:underline transition-colors"
|
||||
>
|
||||
|
||||
@@ -2,7 +2,7 @@ import Link from "next/link";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
|
||||
type StopCardProps = {
|
||||
id: string;
|
||||
slug: string;
|
||||
city: string;
|
||||
state: string;
|
||||
date: string;
|
||||
@@ -13,7 +13,7 @@ type StopCardProps = {
|
||||
};
|
||||
|
||||
export default function StopCard({
|
||||
id, city, state, date, time, location,
|
||||
slug, city, state, date, time, location,
|
||||
brandSlug = "tuxedo", brandAccent = "green",
|
||||
}: StopCardProps) {
|
||||
const ctaBg =
|
||||
@@ -25,7 +25,7 @@ export default function StopCard({
|
||||
|
||||
return (
|
||||
<div className="group overflow-hidden rounded-3xl bg-white ring-1 ring-stone-200/60 transition-all duration-300 hover:-translate-y-1 hover:shadow-xl hover:shadow-black/8 hover:ring-stone-300">
|
||||
<Link href={`/${brandSlug}/stops/${id}`} className="block p-7">
|
||||
<Link href={`/${brandSlug}/stops/${slug}`} className="block p-7">
|
||||
{/* Location */}
|
||||
<div className="flex items-start gap-4 mb-5">
|
||||
<div className={`flex h-10 w-10 flex-shrink-0 items-center justify-center rounded-xl transition-colors ${brandAccent === "blue" ? "bg-blue-50 group-hover:bg-blue-100" : brandAccent === "green" ? "bg-emerald-50 group-hover:bg-emerald-100" : "bg-orange-50 group-hover:bg-orange-100"}`}>
|
||||
@@ -56,7 +56,7 @@ export default function StopCard({
|
||||
{/* CTA */}
|
||||
<div className="px-7 pb-7 pt-1">
|
||||
<Link
|
||||
href={`/${brandSlug}/stops/${id}`}
|
||||
href={`/${brandSlug}/stops/${slug}`}
|
||||
className={`block w-full rounded-2xl ${ctaBg} text-white px-5 py-3.5 text-center font-bold text-sm uppercase tracking-wider transition-colors`}
|
||||
>
|
||||
Shop This Stop
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
@@ -22,6 +25,13 @@ export default function StorefrontFooter({
|
||||
isAdmin?: boolean;
|
||||
brandAccent?: "green" | "orange" | "blue";
|
||||
}) {
|
||||
// The copyright year is computed exactly once via a lazy initializer. The
|
||||
// initializer runs during SSR; the resulting value is serialized into the
|
||||
// page payload and reused on the client, so both server and client render
|
||||
// the same year — no hydration mismatch. (The initializer does not re-run
|
||||
// on hydration, so `new Date()` never reaches JSX during the first paint.)
|
||||
const [year] = useState<number>(() => new Date().getFullYear());
|
||||
|
||||
const accentClass =
|
||||
brandAccent === "orange"
|
||||
? "text-orange-500 hover:text-orange-600"
|
||||
@@ -198,7 +208,7 @@ export default function StorefrontFooter({
|
||||
|
||||
<div className="mt-14 flex flex-col items-center justify-between gap-4 border-t border-stone-200 pt-8 sm:flex-row">
|
||||
<p className="text-sm text-stone-400">
|
||||
© {new Date().getFullYear()} {brandName}. All rights reserved.
|
||||
© {year} {brandName}. All rights reserved.
|
||||
</p>
|
||||
<div className="flex items-center gap-6">
|
||||
{isAdmin && (
|
||||
|
||||
@@ -136,7 +136,7 @@ export default function StorefrontHeader({
|
||||
</Link>
|
||||
|
||||
{/* Mobile Menu Button */}
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
className="flex h-10 w-10 items-center justify-center rounded-xl border border-stone-200 text-stone-500 transition-all duration-200 hover:border-stone-400 hover:text-stone-700 active:scale-95 md:hidden"
|
||||
aria-expanded={menuOpen}
|
||||
|
||||
@@ -145,16 +145,19 @@ export default function StripeExpressCheckout(props: Props) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
// Re-fetch when the inputs that affect the PaymentIntent change.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [
|
||||
sessionKey,
|
||||
props.brandId,
|
||||
props.selectedStop?.id,
|
||||
props.customerName,
|
||||
props.customerEmail,
|
||||
hasShip ? `${props.shippingState}|${props.shippingPostal}|${props.shippingCity}` : "",
|
||||
props.shippingState,
|
||||
props.shippingPostal,
|
||||
props.shippingCity,
|
||||
hasShip,
|
||||
stopBlocked,
|
||||
props.items,
|
||||
props,
|
||||
]);
|
||||
|
||||
// Persist the pending-checkout payload to sessionStorage so the
|
||||
|
||||
@@ -217,7 +217,7 @@ export default function SweetCornProductPage({ brandId, brandName, logoUrl, logo
|
||||
{/* Thumbnails row (decorative — same image) */}
|
||||
<div className="mt-4 flex gap-3">
|
||||
{[0, 1, 2].map((i) => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={i}
|
||||
aria-label={`View image ${i + 1}`}
|
||||
className={`relative aspect-square w-16 sm:w-20 overflow-hidden rounded-xl ring-2 transition-all ${
|
||||
@@ -327,7 +327,7 @@ export default function SweetCornProductPage({ brandId, brandName, logoUrl, logo
|
||||
Qty
|
||||
</span>
|
||||
<div className="inline-flex items-center rounded-full ring-1 ring-stone-900/15 bg-stone-50">
|
||||
<button
|
||||
<button type="button"
|
||||
aria-label="Decrease quantity"
|
||||
className="h-10 w-10 flex items-center justify-center text-stone-700 hover:text-stone-950 hover:bg-stone-100 rounded-l-full transition-colors"
|
||||
onClick={() => {
|
||||
@@ -337,7 +337,7 @@ export default function SweetCornProductPage({ brandId, brandName, logoUrl, logo
|
||||
−
|
||||
</button>
|
||||
<span className="w-10 text-center font-bold text-stone-950">1</span>
|
||||
<button
|
||||
<button type="button"
|
||||
aria-label="Increase quantity"
|
||||
className="h-10 w-10 flex items-center justify-center text-stone-700 hover:text-stone-950 hover:bg-stone-100 rounded-r-full transition-colors"
|
||||
>
|
||||
@@ -583,13 +583,13 @@ export default function SweetCornProductPage({ brandId, brandName, logoUrl, logo
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 shrink-0">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleAddToCart}
|
||||
className="rounded-2xl h-14 px-7 bg-white text-stone-950 font-bold hover:bg-stone-100 active:scale-[0.98] transition-all"
|
||||
>
|
||||
Add to cart — {PRODUCT.price}
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleBuyNow}
|
||||
className="rounded-2xl h-14 px-7 bg-[#FCD34D] text-stone-950 font-bold hover:bg-[#FBBF24] active:scale-[0.98] transition-all"
|
||||
>
|
||||
@@ -611,13 +611,13 @@ export default function SweetCornProductPage({ brandId, brandName, logoUrl, logo
|
||||
{PRODUCT.price}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleBuyNow}
|
||||
className="h-14 min-w-[120px] rounded-2xl bg-[#FFF1D6] text-[#9A4A06] font-bold text-[14px] ring-1 ring-[#E5A80F]/40 active:scale-[0.98] transition-all"
|
||||
>
|
||||
Quick Buy
|
||||
</button>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handleAddToCart}
|
||||
className="h-14 min-w-[160px] rounded-2xl bg-stone-950 text-white font-bold text-[15px] active:scale-[0.98] transition-all"
|
||||
>
|
||||
@@ -704,7 +704,7 @@ function FAQItem({ q, a }: { q: string; a: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<div className="py-5">
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="w-full flex items-center justify-between gap-4 text-left group"
|
||||
>
|
||||
|
||||
@@ -41,15 +41,8 @@ export default function TuxedoVideoHero({
|
||||
const sectionRef = useRef<HTMLElement>(null);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
const [scrollProgress, setScrollProgress] = useState(0);
|
||||
|
||||
// Trigger entrance animation on mount
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setIsVisible(true), 100);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
// ─── GSAP SCROLL ANIMATIONS ────────────────────────────────────────────────
|
||||
// Motion pass: the original used a 1.15 video scale, an 80px y-shift on the
|
||||
// hero content, a parallax -100/+80 movement on decorative orbs, and a 1s
|
||||
@@ -62,13 +55,6 @@ export default function TuxedoVideoHero({
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined" || !sectionRef.current) return;
|
||||
|
||||
// Respect reduced motion preference — render everything visible immediately.
|
||||
const prefersReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches === true;
|
||||
if (prefersReducedMotion) {
|
||||
setIsVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const ctx = gsap.context(() => {
|
||||
// Scroll progress tracking (the bar at the top of the page).
|
||||
ScrollTrigger.create({
|
||||
@@ -244,7 +230,7 @@ export default function TuxedoVideoHero({
|
||||
<motion.div
|
||||
className="hero-reveal mb-10 relative h-20 md:h-24 w-[320px] md:w-[400px]"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={isVisible ? { opacity: 1 } : { opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ duration: 0.32, delay: 0.05, ease: "easeOut" }}
|
||||
>
|
||||
<div className="absolute -inset-8 rounded-full bg-emerald-500/10 blur-2xl" />
|
||||
@@ -279,7 +265,7 @@ export default function TuxedoVideoHero({
|
||||
>
|
||||
{title.split(" ").map((word, idx) => (
|
||||
<span
|
||||
key={idx}
|
||||
key={`${idx}-${word}`}
|
||||
className="inline-block mr-4"
|
||||
style={{
|
||||
animationDelay: `${0.2 + idx * 0.1}s`,
|
||||
@@ -333,7 +319,7 @@ export default function TuxedoVideoHero({
|
||||
</motion.button>
|
||||
)}
|
||||
{secondaryButton && (
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={onSecondaryClick}
|
||||
className="group flex items-center gap-3 text-sm font-semibold"
|
||||
style={{ color: "rgba(255,255,255,0.8)" }}
|
||||
@@ -363,8 +349,8 @@ export default function TuxedoVideoHero({
|
||||
{ stat: "40+", label: "Years Growing" },
|
||||
{ stat: "3", label: "Generations" },
|
||||
{ stat: "100%", label: "Hand-Picked" },
|
||||
].map((item, i) => (
|
||||
<div key={i} className="text-center">
|
||||
].map((item) => (
|
||||
<div key={item.label} className="text-center">
|
||||
<div
|
||||
className="text-3xl md:text-4xl font-black text-white"
|
||||
style={{ textShadow: "0 2px 20px rgba(0,0,0,0.3)" }}
|
||||
@@ -388,7 +374,7 @@ export default function TuxedoVideoHero({
|
||||
className="scroll-indicator absolute bottom-10 left-1/2 -translate-x-1/2 z-20 flex flex-col items-center gap-3"
|
||||
style={{ pointerEvents: "auto" }}
|
||||
>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={handlePrimaryClick}
|
||||
className="flex flex-col items-center gap-2 text-white/50 hover:text-white/80 transition-colors duration-200"
|
||||
aria-label="Scroll to stops"
|
||||
|
||||
@@ -48,7 +48,7 @@ export default function ZipCodeSearch({ allStops = [], brandAccent = "blue" }: Z
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
<input aria-label="ZIP Code Or City..."
|
||||
type="text"
|
||||
placeholder="ZIP code or city..."
|
||||
value={zipCode}
|
||||
@@ -57,7 +57,7 @@ export default function ZipCodeSearch({ allStops = [], brandAccent = "blue" }: Z
|
||||
className="w-full rounded-xl border border-stone-200 pl-11 pr-4 py-3.5 text-sm text-stone-900 placeholder-stone-400 bg-white outline-none focus:border-stone-900 focus:ring-1 focus:ring-stone-900 transition-colors shadow-sm"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
<button type="button"
|
||||
onClick={() => {
|
||||
if (!zipCode.trim()) return;
|
||||
const normalized = zipCode.trim().replace(/\D/g, "");
|
||||
@@ -83,7 +83,7 @@ export default function ZipCodeSearch({ allStops = [], brandAccent = "blue" }: Z
|
||||
{filteredStops.length} stop{filteredStops.length !== 1 ? "s" : ""} found
|
||||
</p>
|
||||
{filteredStops.map((stop) => (
|
||||
<button
|
||||
<button type="button"
|
||||
key={stop.id}
|
||||
onClick={() => router.push(`#stops`)}
|
||||
className="w-full rounded-xl border border-stone-200 bg-stone-50 px-5 py-4 text-left hover:bg-stone-100 group transition-all"
|
||||
|
||||
Reference in New Issue
Block a user