import { useState } from "react"; import { data, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Form, useLoaderData, useNavigation, useSearchParams } from "@remix-run/react"; import { Page, Card, BlockStack, InlineStack, Text, Button, Select, TextField, IndexTable, EmptyState, } from "@shopify/polaris"; import { TitleBar } from "@shopify/app-bridge-react"; import { authenticate } from "../shopify.server"; import db from "../db.server"; import { getShopTier } from "../services/billing.server"; import { tierAtLeast } from "../lib/billing-plans"; import { UpsellState } from "../components/UpsellState"; const ZONE_TYPES = [ { label: "Postal / ZIP codes", value: "postal" }, { label: "Radius (straight-line distance)", value: "radius" }, { label: "Driving distance (via Google Maps)", value: "driving" }, ]; export const loader = async ({ request }: LoaderFunctionArgs) => { const { session } = await authenticate.admin(request); const tier = await getShopTier(session.shop); if (!tierAtLeast(tier, "growth")) { return { gated: true as const, tier }; } const url = new URL(request.url); const locationId = url.searchParams.get("locationId"); const locations = await db.location.findMany({ where: { shopDomain: session.shop }, orderBy: { createdAt: "asc" }, }); const activeLocationId = locationId || locations[0]?.id || null; const zones = activeLocationId ? await db.zone.findMany({ where: { shopDomain: session.shop, locationId: activeLocationId }, orderBy: { createdAt: "asc" }, }) : []; return { gated: false as const, locations, activeLocationId, zones }; }; export const action = async ({ request }: ActionFunctionArgs) => { const { session } = await authenticate.admin(request); const tier = await getShopTier(session.shop); if (!tierAtLeast(tier, "growth")) { return data({ errors: { name: "Delivery zones need the Growth plan or higher." } }, { status: 403 }); } const formData = await request.formData(); const intent = formData.get("intent"); if (intent === "delete") { const id = String(formData.get("id") || ""); await db.zone.deleteMany({ where: { id, shopDomain: session.shop } }); return data({ ok: true }); } const locationId = String(formData.get("locationId") || ""); const name = String(formData.get("name") || "").trim(); const type = String(formData.get("type") || "postal"); const postalCodesRaw = String(formData.get("postalCodes") || ""); const radiusKmRaw = String(formData.get("radiusKm") || ""); const minOrdersRaw = String(formData.get("minOrders") || ""); const errors: Record = {}; if (!locationId) errors.locationId = "Choose a location"; if (!name) errors.name = "Name is required"; if ((type === "radius" || type === "driving") && !radiusKmRaw) errors.radiusKm = "Distance is required for this zone type"; if (Object.keys(errors).length > 0) { return data({ errors }); } const postalCodes = postalCodesRaw .split(",") .map((code) => code.trim()) .filter(Boolean); await db.zone.create({ data: { shopDomain: session.shop, locationId, name, type, postalCodes: type === "postal" ? postalCodes : [], radiusKm: (type === "radius" || type === "driving") && radiusKmRaw ? Number(radiusKmRaw) : null, minOrders: minOrdersRaw ? Number(minOrdersRaw) : null, }, }); return data({ ok: true }); }; export default function ZonesIndex() { const loaderData = useLoaderData(); const [, setSearchParams] = useSearchParams(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; if (loaderData.gated) { return ( ); } const { locations, activeLocationId, zones } = loaderData; if (locations.length === 0) { return ( Delivery zones determine which addresses a location's Local Delivery covers. ); } return ( ))} )} ); } function AddZoneForm({ locationId, isSubmitting }: { locationId: string; isSubmitting: boolean }) { const [type, setType] = useState("postal"); const [name, setName] = useState(""); const [postalCodes, setPostalCodes] = useState(""); const [radiusKm, setRadiusKm] = useState(""); const [minOrders, setMinOrders] = useState(""); return (
Add a delivery zone