bundle-offers-app/app/routes/app.templates.tsx
metatroncubeswdev d4d42b917b Proxy pages carry their own CSS/JS; align festival calendar; App Store screenshots
Bundle pages stay styled and functional when the app embed is off (e.g. on a draft
theme). Listing screenshots captured from the dev store and rendered at 1600x900.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:26:04 -04:00

73 lines
3.3 KiB
TypeScript

import type { HeadersFunction, LoaderFunctionArgs } from "react-router";
import { useLoaderData } from "react-router";
import { boundary } from "@shopify/shopify-app-react-router/server";
import { authenticate } from "../shopify.server";
import { ensureShop } from "../lib/shop.server";
import { planOf } from "../lib/plans";
import { festivalByName, TEMPLATES, upcomingFestivals } from "../lib/templates";
import { moneyFormatter, offerSummary, TYPE_LABEL } from "../lib/offer-text";
export const loader = async ({ request }: LoaderFunctionArgs) => {
const { admin, session } = await authenticate.admin(request);
const shop = await ensureShop(admin, session.shop);
const plan = planOf(shop.planCode);
const money = moneyFormatter(shop.currency);
const now = new Date();
return {
allowed: plan.templates,
calendar: upcomingFestivals(now).map((f) => ({
...f,
weeksAway: Math.round((new Date(`${f.date}T00:00:00Z`).getTime() - now.getTime()) / (7 * 86400000)),
})),
templates: TEMPLATES.map((t) => ({
...t,
summary: offerSummary({ ...t, mixQty: t.mixQty ?? 3, buyQty: t.buyQty ?? 2, getQty: t.getQty ?? 1, getPercent: t.getPercent ?? 100, active: true, published: true, startsAt: null, endsAt: null }, money),
next: festivalByName(t.festival)?.date || "",
})),
};
};
export default function Templates() {
const { templates, calendar, allowed } = useLoaderData<typeof loader>();
return (
<s-page heading="Festival templates" inlineSize="large">
{!allowed ? (
<s-banner tone="info" heading="Templates and the festival calendar are on the Growth plan">
<s-button slot="secondary-actions" href="/app/plans">See plans</s-button>
</s-banner>
) : null}
<s-section heading="Festival calendar">
<s-stack gap="small-200">
{calendar.map((f) => (
<s-grid key={f.key} gridTemplateColumns="1fr 1fr auto" gap="base" alignItems="center">
<s-text type="strong">{f.name}</s-text>
<s-text>{f.date}{f.approximate ? " (approx.)" : ""}</s-text>
<s-badge tone={f.weeksAway <= 6 ? "caution" : "neutral"}>{f.weeksAway <= 0 ? "This week" : `In ${f.weeksAway} weeks`}</s-badge>
</s-grid>
))}
<s-text color="subdued">Plan your offers about 6 weeks ahead. Lunar dates are approximate: check them locally.</s-text>
</s-stack>
</s-section>
<s-section heading="Templates">
<s-grid gridTemplateColumns="repeat(auto-fill, minmax(260px, 1fr))" gap="base">
{templates.map((t) => (
<s-box key={t.key} padding="base" borderWidth="base" borderRadius="base">
<s-stack gap="small-200">
<s-stack direction="inline" gap="small-200"><s-badge>{t.badge}</s-badge><s-badge tone="info">{TYPE_LABEL[t.type]}</s-badge></s-stack>
<s-heading>{t.name}</s-heading>
<s-text>{t.summary}</s-text>
<s-text color="subdued">{t.festival}{t.next ? ` · ${t.next}` : ""}</s-text>
<s-button href={`/app/offers/new?template=${t.key}`} disabled={!allowed}>Use template</s-button>
</s-stack>
</s-box>
))}
</s-grid>
</s-section>
</s-page>
);
}
export const headers: HeadersFunction = (headersArgs) => boundary.headers(headersArgs);