Webáruház
Hol jelenik meg egy app a webáruházban. Blokkok, amelyeket a kereskedők a témaszerkesztőben helyeznek el, header- és lábléczónák, webáruházi oldalak, a kosár, és amit egy témának támogatnia kell.
Ezen az oldalon
A webáruházban az appok háromféle helyen jelenítenek meg natív workereket: a kereskedő által az oldalsablonokon elhelyezett blokkokban, a téma header- és lábléczónáiban, valamint teljes webáruházi oldalakon a /apps/<slug> címen.
A worker csak ott jelenik meg, ahová a kereskedő a közzétett témában elhelyezte, és csak addig, amíg az app telepítve van. A blokk eltávolítása vagy áthelyezése, a beállításainak vagy az app release-ének módosítása, vagy az app eltávolítása leveszi az oldalról; a független témamódosítások közzététele viszont nem. Ha egy worker betöltése meghiúsul, a vásárlók hibaüzenet helyett egyszerűen semmit sem látnak a helyén.
Blokkok
A storefront.home, storefront.products, storefront.product, storefront.cart, storefront.checkout, storefront.login, storefront.register, storefront.account, storefront.contact és storefront.page célú workerek az adott oldalsablon blokkjai. A témaszerkesztőben minden gyári téma engedi, hogy a kereskedők hozzáadják őket a szekciókhoz ezeken a sablonokon.
Minden blokk a @poltura/app-sdk/elements csomagból származó defineBlock függvénnyel írja le magát: a nevet, az ikont és a kategóriát a témaszerkesztő választójában, a beállításait és a kereskedőknek megjelenő stílusvezérlőket. Sorold fel a definíciókat tartalmazó fájlokat a manifest elements mezőjében, és a blokk type értékét a worker element mezőjében; a build beolvassa őket a release-be. Lásd: Beállítások és fordítások.
export const recommendationsPanelElement = defineBlock({
type: 'recommendations-panel',
label: 'Recommendations',
settings: {
heading: field.text({ label: 'Heading', default: 'Recommendations' }),
max_items: field.number({ label: 'Item limit', min: 1, max: 8, default: 4 }),
},
});
export const recommendationsElement = defineBlock({
type: 'recommendations',
label: 'You may also like',
description: 'Recommended products next to the current product.',
icon: 'product-grid',
category: 'commerce',
settings: {
heading: field.text({ label: 'Block heading', default: 'You may also like' }),
variant_id: field.text({
label: 'Featured variant',
default: '0199f3a2-af74-7051-b24d-6f8b1c3d4e5f',
}),
},
style: ['spacing', 'background', 'border'],
presets: [
{
id: 'quiet',
label: 'Quiet',
settings: { heading: 'More to explore' },
},
],
});
export const newsletterCaptureElement = defineBlock({
type: 'newsletter-capture',
label: 'Newsletter capture',
icon: 'mail',
category: 'forms',
style: ['spacing', 'background'],
});
export const recommendationsPageElement = defineBlock({
type: 'recommendations-page',
label: 'Recommendations page',
settings: {
heading: field.text({ label: 'Page heading', default: 'Recommendations' }),
},
});export function StorefrontProduct(): ReactNode {
const copy = copyFor(useLocale());
const settings = useSettings();
const theme = useTheme();
const cart = useCart();
const toast = useToast();
const host = useStorefrontContext();
const recommend = useAction('recommend');
const heading = String(settings.heading ?? copy.alsoLike);
const storeName = theme?.settings?.store_name;
const [contextLabel, setContextLabel] = useState(copy.alsoLike);
useEffect(() => {
void host.read().then(async (context) => {
if (context.kind === 'product') setContextLabel(context.product.title);
const suggestion = await recommend({
productId: context.kind === 'product' ? context.product.id : null,
});
const title = z.object({ title: z.string().min(1) }).safeParse(suggestion);
if (title.success) setContextLabel(title.data.title);
});
}, [host, recommend]);
return (
<Card className='offer-panel rounded-theme bg-surface text-foreground p-4'>
<Stack>
<Heading className='font-display'>{heading}</Heading>
<Text>{z.string().min(1).safeParse(storeName).data ?? contextLabel}</Text>
<Button
label={copy.addToCart}
className='bg-primary text-on-primary'
onPress={async () => {
const parsed = VariantId.safeParse(settings.variant_id);
if (!parsed.success) {
await toast.show({ tone: 'warning', message: copy.cartRejected });
return;
}
const reply = await cart.add({ variantId: parsed.data, quantity: 1 });
if (reply.status === 'ok') {
await toast.show({ tone: 'success', message: copy.added });
return;
}
await toast.show({ tone: 'warning', message: copy.cartRejected });
}}
/>
</Stack>
</Card>
);
}useStorefrontContext().read()megadja a blokknak, hogy hol van:{ kind: 'product', product, selectedVariantId? }egy termékoldalon,{ kind: 'collection', collection }egy gyűjteményoldalon,{ kind: 'cart', cart }a kosárban,{ kind: 'page', path }máshol, vagy{ kind: 'none' }.useCart()beolvassa és módosítja a vásárló kosarát aread,add,updateésremovemetódusokkal, elemenként egyszerre 1 és 99 közötti darabszámmal. Egy opcionális idempotenciakulcs (legfeljebb 64 betű, számjegy,_és-) biztosítja, hogy az újrapróbált módosítás csak egyszer érvényesüljön. A kosárhívások csak a webáruházban működnek.useAction('recommend')ugyanúgy a Poltura által aláírt kéréssel hívja meg a szerveredet, ahogy egy adminfelületi worker teszi. A webáruházi hívások a vásárló adatait is átadják.
A workerek nem tudják közvetlenül olvasni az app tárhelyét. A példa a kiemelt változatot a blokk beállításaiban tárolja, a szervere pedig a teljes listát a tárhelyen tartja.
Header- és lábléczónák
A storefront.layout.header és storefront.layout.footer célú workerek a téma headerében és láblécében jelennek meg. Minden gyári témában megvan mindkét zóna. A headerzóna saját, teljes szélességű sorban helyezkedik el a navigáció alatt, így egy magasabb worker lejjebb tolja az oldalt ahelyett, hogy eltakarná.
export function StorefrontFooter(): ReactNode {
const copy = copyFor(useLocale());
const toast = useToast();
const [email, setEmail] = useState('');
const [error, setError] = useState('');
return (
<Card className='rounded-theme bg-muted text-foreground p-4'>
<Stack>
<Text>{copy.footerHelp}</Text>
<TextField
label={copy.emailLabel}
value={email}
error={error}
onChange={(value) => {
setEmail(value);
setError('');
}}
/>
<Button
label={copy.subscribe}
onPress={async () => {
const parsed = Email.safeParse(email);
if (!parsed.success) {
setError(copy.invalidEmail);
await toast.show({ tone: 'warning', message: copy.invalidEmail });
return;
}
setError('');
await toast.show({ tone: 'success', message: copy.subscribed });
}}
/>
</Stack>
</Card>
);
}A lábléc űrlapja a böngészőben ellenőrzi a címet, és nem küldi el sehová.
Webáruházi oldalak
A storefront.app-page célú worker a bolt oldalává válik a /apps/<slug> címen, amint szerepel a manifest storefrontPages listájában (legfeljebb 8 ilyen oldal adható meg). Kimarad a sitemapből, kivéve, ha a seo.indexable értéke true. A kereskedők megtalálják ezeket az oldalakat a témaszerkesztőben, és hivatkozhatnak rájuk a menükből.
export function AppPage(): ReactNode {
const copy = copyFor(useLocale());
const settings = useSettings();
const navigate = useNavigate();
const heading = String(settings.heading ?? copy.heading);
return (
<Card className='rounded-theme bg-surface text-foreground p-4'>
<Stack>
<Heading>{heading}</Heading>
<Text>{copy.pageIntro}</Text>
<Button label={copy.shopProducts} onPress={() => navigate.to('/products')} />
</Stack>
</Card>
);
}A storefrontPages bejegyzés nélkül a worker bekerül a bundle-be, de egyetlen cím sem szolgálja ki.
Amit egy témának támogatnia kell
A Poltura minden gyári témája támogatja az appokat. Egy saját készítésű témában a következők kellenek az appok működéséhez:
- szekciók, amelyek bármilyen blokkot elfogadnak (
ELEMENT_WILDCARD) a fenti sablonokon; - a
layout.headeréslayout.footerzónák; - a
/apps/<slug>route-ok a layoutján belül.