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>
73 lines
3.3 KiB
TypeScript
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);
|