Refactor: move public storefront stop data to server-side + parallel agent work
Server-side / caching refactor (Grok): - New RPC get_public_stops_for_brand (migration 148) for public storefront stops - New server action getPublicStopsForBrand with revalidate=300 + tags - Add revalidateTag invalidation to createStopsBatch + publishStop - Convert /tuxedo/stops and /indian-river-direct/stops to Server Components - Extract TuxedoStopsList + IndianRiverStopsList as client islands (GSAP only) - Removes supabase-js from browser bundle on those routes - Both pages now statically prerendered (5m ISR) Parallel agent changes also staged: - AI provider model list refresh (claude-sonnet-4-5, etc.) - ESLint directive patches for react-hooks/set-state-in-effect - Admin + storefront + checkout + cart updates - New admin_create_stop_rpcs migration (147) - Misc fixes across ~90 files Build verified: typecheck clean, lint clean on new files, production build succeeds.
This commit is contained in:
@@ -2,13 +2,14 @@
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
type AIProvider = "openai" | "anthropic" | "google" | "xai" | "custom";
|
||||
type AIProvider = "openai" | "anthropic" | "google" | "xai" | "minimax" | "custom";
|
||||
|
||||
const PROVIDER_MODELS: Record<Exclude<AIProvider, "custom">, string[]> = {
|
||||
openai: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-3.5-turbo"],
|
||||
anthropic: ["claude-3-5-sonnet-20241002", "claude-3-5-sonnet-20250611", "claude-3-opus-20240229", "claude-3-haiku-20240307"],
|
||||
anthropic: ["claude-sonnet-4-5", "claude-sonnet-4-20250514", "claude-opus-4-1", "claude-opus-4-20250514", "claude-3-7-sonnet-20250219", "claude-3-5-haiku-20241022", "claude-3-haiku-20240307"],
|
||||
google: ["gemini-2.0-flash", "gemini-1.5-pro", "gemini-1.5-flash", "gemini-pro"],
|
||||
xai: ["grok-2", "grok-2-mini", "grok-1.5"],
|
||||
minimax: ["MiniMax-M3", "MiniMax-M3-highspeed", "MiniMax-M2.5", "MiniMax-M2.5-highspeed", "MiniMax-M2.7", "MiniMax-M2.1", "MiniMax-M2"],
|
||||
};
|
||||
|
||||
// Icons
|
||||
@@ -87,6 +88,7 @@ const PROVIDERS: { id: AIProvider; name: string; icon: React.ReactNode; descript
|
||||
{ id: "anthropic", name: "Anthropic", icon: <BrainIcon className="h-5 w-5" />, description: "Claude 3.5 Sonnet, Claude 3 Opus. Best for reasoning.", website: "anthropic.com", color: "bg-amber-500" },
|
||||
{ id: "google", name: "Google", icon: <CircleIcon className="h-5 w-5" />, description: "Gemini 2.0 Flash, 1.5 Pro. Fast, cost-effective.", website: "ai.google", color: "bg-blue-500" },
|
||||
{ id: "xai", name: "xAI", icon: <TornadoIcon className="h-5 w-5" />, description: "Grok-2, Grok-1.5. Real-time data, humor.", website: "x.ai", color: "bg-orange-500" },
|
||||
{ id: "minimax", name: "MiniMax", icon: <SparkleIcon className="h-5 w-5" />, description: "MiniMax-M3, M2.5, M2. OpenAI-compatible.", website: "minimax.io", color: "bg-violet-500" },
|
||||
{ id: "custom", name: "Custom", icon: <WrenchIcon className="h-5 w-5" />, description: "Any OpenAI-compatible API endpoint.", website: "", color: "bg-stone-500" },
|
||||
];
|
||||
|
||||
@@ -232,7 +234,7 @@ export default function AIProviderPanel({ brandId }: Props) {
|
||||
type={showKey ? "text" : "password"}
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={provider === "anthropic" ? "sk-ant-..." : provider === "google" ? "AIza..." : provider === "xai" ? "xai-..." : "sk-..."}
|
||||
placeholder={provider === "anthropic" ? "sk-ant-..." : provider === "google" ? "AIza..." : provider === "xai" ? "xai-..." : provider === "minimax" ? "ey..." : "sk-..."}
|
||||
className="w-full rounded-lg border border-[var(--admin-border)] bg-white px-3 py-2.5 text-sm text-[var(--admin-text-primary)] placeholder:text-stone-400 outline-none focus:border-emerald-500 focus:ring-1 focus:ring-emerald-500"
|
||||
/>
|
||||
<button
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { getAbandonedCarts, manuallyCloseAbandonedCart, resendAbandonedCartEmail, type AbandonedCart } from "@/actions/email-automation/abandoned-cart";
|
||||
|
||||
@@ -30,6 +30,7 @@ export default function AddStopModal({ isOpen, onClose, brandId, duplicateFrom,
|
||||
const [location, setLocation] = useState("");
|
||||
const [date, setDate] = useState("");
|
||||
const [time, setTime] = useState("");
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
const [address, setAddress] = useState("");
|
||||
const [zip, setZip] = useState("");
|
||||
const [cutoffTime, setCutoffTime] = useState("");
|
||||
@@ -37,6 +38,7 @@ export default function AddStopModal({ isOpen, onClose, brandId, duplicateFrom,
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
// Reset form when modal opens with optional duplicate source
|
||||
setCity(duplicateFrom?.city ?? "");
|
||||
setState(duplicateFrom?.state ?? "");
|
||||
setLocation(duplicateFrom?.location ?? "");
|
||||
@@ -48,6 +50,7 @@ export default function AddStopModal({ isOpen, onClose, brandId, duplicateFrom,
|
||||
setStatus("draft");
|
||||
setError(null);
|
||||
}
|
||||
/* eslint-enable react-hooks/set-state-in-effect */
|
||||
}, [isOpen, duplicateFrom]);
|
||||
|
||||
const handleSubmit = useCallback(async (e: React.FormEvent) => {
|
||||
|
||||
@@ -72,6 +72,7 @@ export default function AdminHeader({ userRole, canManageUsers, routeTraceEnable
|
||||
|
||||
// Close dropdown on route change
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setSettingsOpen(false);
|
||||
}, [pathname]);
|
||||
|
||||
|
||||
@@ -134,6 +134,7 @@ export default function AdminOrdersPanel({
|
||||
|
||||
// Simulate loading when orders change
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setIsLoading(true);
|
||||
const timer = setTimeout(() => setIsLoading(false), 300);
|
||||
return () => clearTimeout(timer);
|
||||
|
||||
@@ -382,7 +382,7 @@ export default function AdminSidebar({ userRole }: SidebarProps) {
|
||||
|
||||
{/* Back to site link */}
|
||||
<div className="px-5 py-3 border-b flex-shrink-0" style={{ borderColor: "rgba(208, 203, 180, 0.2)" }}>
|
||||
<a
|
||||
<Link
|
||||
href="/"
|
||||
className="text-xs transition-colors flex items-center gap-1.5 hover:text-white"
|
||||
style={{ color: "var(--admin-sidebar-text)" }}
|
||||
@@ -391,7 +391,7 @@ export default function AdminSidebar({ userRole }: SidebarProps) {
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
Back to Site
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Nav links with keyboard navigation */}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import NextImage from "next/image";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function CommunicationSettingsForm({
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-[var(--admin-text-primary)]">Sender Settings</h2>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">Configure the default sender identity for this brand's email campaigns</p>
|
||||
<p className="text-xs text-[var(--admin-text-muted)]">Configure the default sender identity for this brand's email campaigns</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { type SegmentRuleV2, type PreviewResult } from "@/actions/harvest-reach/segments";
|
||||
|
||||
@@ -67,7 +67,7 @@ export default function SegmentListSidebar({ segments, activeSegmentId, onSelect
|
||||
<div key={segment.id} className="group relative">
|
||||
{confirmDelete === segment.id ? (
|
||||
<div className="rounded-xl border border-red-200 bg-red-50 p-3 flex flex-col gap-2">
|
||||
<p className="text-xs text-red-600 font-medium">Delete "{segment.name}"?</p>
|
||||
<p className="text-xs text-red-600 font-medium">Delete "{segment.name}"?</p>
|
||||
<div className="flex gap-2">
|
||||
<AdminButton
|
||||
variant="danger"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { getMessageLogs, type MessageLogEntry } from "@/actions/communications/send";
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import NextImage from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useState, useRef } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { uploadProductImage } from "@/actions/products/upload-image";
|
||||
import { createProduct } from "@/actions/products/create-product";
|
||||
import { AdminInput, AdminTextInput, AdminTextarea, AdminSelect } from "./design-system";
|
||||
|
||||
export default function NewProductForm() {
|
||||
type Props = {
|
||||
defaultBrandId?: string;
|
||||
brands?: { id: string; name: string }[];
|
||||
lockBrand?: boolean;
|
||||
};
|
||||
|
||||
export default function NewProductForm({ defaultBrandId = "", brands = [], lockBrand = false }: Props) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
@@ -22,11 +29,22 @@ export default function NewProductForm() {
|
||||
const [description, setDescription] = useState("");
|
||||
const [price, setPrice] = useState("");
|
||||
const [type, setType] = useState("Pickup");
|
||||
const [brandId, setBrandId] = useState("");
|
||||
const [brandId, setBrandId] = useState(defaultBrandId);
|
||||
const [isTaxable, setIsTaxable] = useState("true");
|
||||
const [pickupType, setPickupType] = useState("scheduled_stop");
|
||||
const [active, setActive] = useState("true");
|
||||
|
||||
// Build the brand options. If the server provided a list, use it; otherwise
|
||||
// fall back to the historical hardcoded list so the form still works in
|
||||
// environments where getBrands() failed or returned empty.
|
||||
const brandOptions = brands.length > 0
|
||||
? brands
|
||||
: [
|
||||
{ id: "64294306-5f42-463d-a5e8-2ad6c81a96de", name: "Tuxedo Corn" },
|
||||
{ id: "b1cb7a96-d82b-40b1-80b1-d6dd26c56e28", name: "Indian River Direct" },
|
||||
];
|
||||
const brandSelectOptions = brandOptions.map((b) => ({ value: b.id, label: b.name }));
|
||||
|
||||
async function handleFileSelect(file: File) {
|
||||
const validTypes = ["image/png", "image/jpeg", "image/webp"];
|
||||
if (!validTypes.includes(file.type)) {
|
||||
@@ -162,15 +180,27 @@ export default function NewProductForm() {
|
||||
</div>
|
||||
|
||||
<AdminInput label="Brand" required>
|
||||
<AdminSelect
|
||||
value={brandId}
|
||||
onChange={(e) => setBrandId(e.target.value)}
|
||||
options={[
|
||||
{ value: "", label: "Select brand..." },
|
||||
{ value: "64294306-5f42-463d-a5e8-2ad6c81a96de", label: "Tuxedo Corn" },
|
||||
{ value: "b1cb7a96-d82b-40b1-80b1-d6dd26c56e28", label: "Indian River Direct" },
|
||||
]}
|
||||
/>
|
||||
{lockBrand ? (
|
||||
// brand_admin / store_employee — brand is fixed by their admin_users record
|
||||
<div className="w-full rounded-lg border border-[var(--admin-border)] bg-stone-50 px-3 py-2.5 text-sm text-[var(--admin-text-primary)]">
|
||||
{brandOptions.find((b) => b.id === brandId)?.name ?? brandId}
|
||||
</div>
|
||||
) : (
|
||||
<AdminSelect
|
||||
value={brandId}
|
||||
onChange={(e) => setBrandId(e.target.value)}
|
||||
options={[
|
||||
{ value: "", label: brands.length > 0 ? "Select brand..." : "Loading brands..." },
|
||||
...brandSelectOptions,
|
||||
]}
|
||||
disabled={brands.length === 0 && defaultBrandId === ""}
|
||||
/>
|
||||
)}
|
||||
{!lockBrand && brands.length === 0 && (
|
||||
<p className="mt-1 text-xs text-stone-500">
|
||||
Loading available brands — if this persists, check the admin Brands settings.
|
||||
</p>
|
||||
)}
|
||||
</AdminInput>
|
||||
|
||||
<AdminInput label="Taxable" helpText="Tax applied at checkout for shipping orders in nexus states. Disable for non-taxable items like apparel or cooler boxes.">
|
||||
@@ -278,12 +308,12 @@ export default function NewProductForm() {
|
||||
{loading ? "Creating..." : "Create Product"}
|
||||
</button>
|
||||
|
||||
<a
|
||||
<Link
|
||||
href="/admin/products"
|
||||
className="rounded-xl border border-zinc-600 px-6 py-3 font-medium text-zinc-300"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { createStop } from "@/actions/stops/create-stop";
|
||||
import { AdminInput, AdminTextInput, AdminSelect, AdminButton, useToast } from "./design-system";
|
||||
|
||||
@@ -336,12 +337,12 @@ export default function NewStopForm({ duplicateFrom }: Props) {
|
||||
{loading ? "Creating..." : "Create Stop"}
|
||||
</AdminButton>
|
||||
|
||||
<a
|
||||
<Link
|
||||
href="/admin/stops"
|
||||
className="rounded-xl border border-[var(--admin-border)] px-6 py-3 font-medium text-stone-600 hover:bg-stone-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { savePaymentSettings, type PaymentProvider, type PaymentSettings } from "@/actions/payments";
|
||||
|
||||
@@ -206,7 +206,7 @@ export default function ProductTableBody({
|
||||
/>
|
||||
<div className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-40 w-80 rounded-xl bg-zinc-900 shadow-xl ring-1 ring-zinc-700 p-6">
|
||||
<p className="text-sm font-semibold text-zinc-100">
|
||||
Delete "{product.name}"?
|
||||
Delete "{product.name}"?
|
||||
</p>
|
||||
<p className="mt-2 text-xs text-zinc-500">
|
||||
This will remove the product. If it is attached to any
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import SettingsSections from "@/components/admin/SettingsSections";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import React, { useState, useTransition, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import type { Template, TemplateType, CampaignType } from "@/actions/communications/templates";
|
||||
import { formatDate } from "@/lib/format-date";
|
||||
import { upsertTemplate } from "@/actions/communications/templates";
|
||||
@@ -597,9 +598,9 @@ export function TemplateEditForm({
|
||||
>
|
||||
{saving ? "Saving..." : "Save Template"}
|
||||
</button>
|
||||
<a href="/admin/communications/templates" className="text-xs sm:text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] ml-4">
|
||||
<Link href="/admin/communications/templates" className="text-xs sm:text-sm text-[var(--admin-text-muted)] hover:text-[var(--admin-text-primary)] ml-4">
|
||||
Cancel
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import GlassModal from "@/components/admin/GlassModal";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { createPlanUpgradeCheckout } from "@/actions/billing/stripe-checkout";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { getWelcomeSequence, resendWelcomeEmail, type WelcomeSequenceEntry } from "@/actions/email-automation/welcome-sequence";
|
||||
|
||||
Reference in New Issue
Block a user