import { data, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Form, useLoaderData, useNavigation } from "@remix-run/react"; import { Page, Layout, Text, Card, BlockStack, EmptyState, InlineStack, Badge, Button } 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"; interface OnboardingSettings { onboarding?: { widgetDone?: boolean }; } export const loader = async ({ request }: LoaderFunctionArgs) => { const { session } = await authenticate.admin(request); const [locations, shop, tier] = await Promise.all([ db.location.findMany({ where: { shopDomain: session.shop }, include: { _count: { select: { slotTemplates: true } } }, }), db.shop.findUnique({ where: { shopDomain: session.shop }, select: { settings: true } }), getShopTier(session.shop), ]); const zonesEligible = tierAtLeast(tier, "growth"); const zonesCount = zonesEligible ? await db.zone.count({ where: { shopDomain: session.shop } }) : 0; const settings = (shop?.settings as OnboardingSettings | null) ?? {}; return { locations, tier, checklist: { hasLocation: locations.length > 0, hasSlots: locations.some((l) => l._count.slotTemplates > 0), widgetDone: settings.onboarding?.widgetDone ?? false, zonesEligible, hasZones: zonesCount > 0, }, }; }; // Only the widget step needs an action — every other checklist item is // "done" once its own underlying data exists (a Location, a SlotTemplate, // a Zone), which those pages already create. Theme customization isn't // something this app's data can detect, so it's a manual acknowledgment // stored in Shop.settings.onboarding, the same JSON field templates.server.ts // already uses for widgetCopy. export const action = async ({ request }: ActionFunctionArgs) => { const { session } = await authenticate.admin(request); const formData = await request.formData(); if (formData.get("intent") === "ack-widget") { const shop = await db.shop.findUnique({ where: { shopDomain: session.shop }, select: { settings: true } }); const settings = (shop?.settings as OnboardingSettings | null) ?? {}; const nextSettings = { ...settings, onboarding: { ...settings.onboarding, widgetDone: true } }; await db.shop.upsert({ where: { shopDomain: session.shop }, create: { shopDomain: session.shop, settings: nextSettings }, update: { settings: nextSettings }, }); return data({ ok: true }); } return data({ ok: false }, { status: 400 }); }; interface ChecklistStep { title: string; description: string; done: boolean; action: { label: string; url: string } | { label: string; formIntent: string } | null; } export default function Index() { const { locations, checklist } = useLoaderData(); const navigation = useNavigation(); const isSubmitting = navigation.state === "submitting"; const steps: ChecklistStep[] = [ { title: "Add a location", description: "A fulfillment point — a store, warehouse, or kitchen — that ships, delivers, or hands off orders.", done: checklist.hasLocation, action: { label: "Add location", url: "/app/locations/new" }, }, { title: "Configure weekly slots", description: "Set the days and time windows each location is open for Shipping, Local Delivery, or Pickup.", done: checklist.hasSlots, action: { label: "Set up slots", url: "/app/slots" }, }, { title: "Enable the widget on your storefront", description: 'Add the "Date & time picker" app block to your product or cart page from Online Store > Themes > Customize.', done: checklist.widgetDone, action: { label: "Mark as done", formIntent: "ack-widget" }, }, ...(checklist.zonesEligible ? [ { title: "Set up delivery zones (optional)", description: "Route Local Delivery orders to the nearest location by postal code or radius.", done: checklist.hasZones, action: { label: "Set up zones", url: "/app/zones" }, }, ] : []), ]; const completedCount = steps.filter((s) => s.done).length; return ( {completedCount < steps.length && ( Get set up {`${completedCount}/${steps.length} done`} {steps.map((step) => ( {step.done ? "Done" : "To do"} {step.title} {step.description} {!step.done && step.action && ( "url" in step.action ? ( ) : (
) )}
))}
)} {locations.length === 0 ? ( Set up a fulfillment location and its weekly slots to start taking scheduled Shipping, Local Delivery, or Pickup orders. ) : ( {locations.length} location{locations.length === 1 ? "" : "s"} configured {locations.map((location) => ( {location.name} {`${location._count.slotTemplates} slot templates`} ))} )}
); }