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:
Nora
2026-06-25 23:49:37 -06:00
parent 4d295ef062
commit 0ac4beaaa8
580 changed files with 52565 additions and 4953 deletions
+1 -1
View File
@@ -46,7 +46,7 @@ export default function BreadcrumbNav({
return (
<li
key={index}
key={`${item.label}-${item.href ?? ""}`}
className="flex items-center"
>
{index > 0 && (
+2 -2
View File
@@ -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"
>
+6 -6
View File
@@ -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"
+4 -4
View File
@@ -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"
+3 -3
View File
@@ -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>
+2 -2
View File
@@ -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"
>
+4 -4
View File
@@ -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
+11 -1
View File
@@ -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"
>
+6 -20
View File
@@ -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"
+3 -3
View File
@@ -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"