@zalify/storefront-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +49 -0
- package/README.md +63 -0
- package/dist/commerce/countdown.d.ts +18 -0
- package/dist/commerce/countdown.js +28 -0
- package/dist/commerce/events.d.ts +29 -0
- package/dist/commerce/events.js +14 -0
- package/dist/commerce/facets.d.ts +27 -0
- package/dist/commerce/facets.js +71 -0
- package/dist/commerce/index.d.ts +11 -0
- package/dist/commerce/index.js +11 -0
- package/dist/commerce/money.d.ts +51 -0
- package/dist/commerce/money.js +83 -0
- package/dist/commerce/product.d.ts +102 -0
- package/dist/commerce/product.js +93 -0
- package/dist/editor/frame.d.ts +57 -0
- package/dist/editor/frame.js +241 -0
- package/dist/editor/host.d.ts +31 -0
- package/dist/editor/host.js +67 -0
- package/dist/editor/index.d.ts +9 -0
- package/dist/editor/index.js +9 -0
- package/dist/react/adapter.d.ts +49 -0
- package/dist/react/adapter.js +23 -0
- package/dist/react/blocks/_box.d.ts +10 -0
- package/dist/react/blocks/_box.js +17 -0
- package/dist/react/blocks/_slide.d.ts +24 -0
- package/dist/react/blocks/_slide.js +21 -0
- package/dist/react/blocks/_video-card.d.ts +11 -0
- package/dist/react/blocks/_video-card.js +39 -0
- package/dist/react/blocks/accordion.d.ts +12 -0
- package/dist/react/blocks/accordion.js +5 -0
- package/dist/react/blocks/button.d.ts +10 -0
- package/dist/react/blocks/button.js +15 -0
- package/dist/react/blocks/countdown.d.ts +9 -0
- package/dist/react/blocks/countdown.js +64 -0
- package/dist/react/blocks/custom-liquid.d.ts +11 -0
- package/dist/react/blocks/custom-liquid.js +4 -0
- package/dist/react/blocks/group.d.ts +9 -0
- package/dist/react/blocks/group.js +8 -0
- package/dist/react/blocks/image.d.ts +8 -0
- package/dist/react/blocks/image.js +7 -0
- package/dist/react/blocks/message.d.ts +14 -0
- package/dist/react/blocks/message.js +5 -0
- package/dist/react/blocks/quote.d.ts +9 -0
- package/dist/react/blocks/quote.js +5 -0
- package/dist/react/blocks/spacer.d.ts +8 -0
- package/dist/react/blocks/spacer.js +5 -0
- package/dist/react/blocks/story.d.ts +17 -0
- package/dist/react/blocks/story.js +7 -0
- package/dist/react/blocks/text.d.ts +9 -0
- package/dist/react/blocks/text.js +5 -0
- package/dist/react/blocks/video.d.ts +17 -0
- package/dist/react/blocks/video.js +56 -0
- package/dist/react/components/Facets.d.ts +21 -0
- package/dist/react/components/Facets.js +220 -0
- package/dist/react/components/Icon.d.ts +12 -0
- package/dist/react/components/Icon.js +66 -0
- package/dist/react/components/PaymentIcons.d.ts +5 -0
- package/dist/react/components/PaymentIcons.generated.d.ts +22 -0
- package/dist/react/components/PaymentIcons.generated.js +21 -0
- package/dist/react/components/PaymentIcons.js +18 -0
- package/dist/react/components/Price.d.ts +24 -0
- package/dist/react/components/Price.js +9 -0
- package/dist/react/components/ProductBadges.d.ts +24 -0
- package/dist/react/components/ProductBadges.js +57 -0
- package/dist/react/components/ProductCard.d.ts +17 -0
- package/dist/react/components/ProductCard.js +202 -0
- package/dist/react/components/Rating.d.ts +11 -0
- package/dist/react/components/Rating.js +35 -0
- package/dist/react/components/SwatchStyle.d.ts +23 -0
- package/dist/react/components/SwatchStyle.js +22 -0
- package/dist/react/components/VideoModal.d.ts +55 -0
- package/dist/react/components/VideoModal.js +102 -0
- package/dist/react/engine/CssVariables.d.ts +1 -0
- package/dist/react/engine/CssVariables.js +105 -0
- package/dist/react/engine/context.d.ts +43 -0
- package/dist/react/engine/context.js +45 -0
- package/dist/react/engine/fonts.d.ts +15 -0
- package/dist/react/engine/fonts.js +33 -0
- package/dist/react/engine/images.d.ts +24 -0
- package/dist/react/engine/images.js +40 -0
- package/dist/react/engine/render.d.ts +29 -0
- package/dist/react/engine/render.js +87 -0
- package/dist/react/engine/settings.d.ts +8 -0
- package/dist/react/engine/settings.js +68 -0
- package/dist/react/engine/store.d.ts +64 -0
- package/dist/react/engine/store.js +42 -0
- package/dist/react/engine/translate.d.ts +7 -0
- package/dist/react/engine/translate.js +33 -0
- package/dist/react/engine/types.d.ts +128 -0
- package/dist/react/engine/types.js +1 -0
- package/dist/react/index.d.ts +38 -0
- package/dist/react/index.js +39 -0
- package/dist/react/registries.d.ts +10 -0
- package/dist/react/registries.js +68 -0
- package/dist/react/registries.server.d.ts +2 -0
- package/dist/react/registries.server.js +98 -0
- package/dist/react/sections/404.d.ts +10 -0
- package/dist/react/sections/404.js +10 -0
- package/dist/react/sections/announcement-bar.d.ts +14 -0
- package/dist/react/sections/announcement-bar.js +84 -0
- package/dist/react/sections/article.d.ts +17 -0
- package/dist/react/sections/article.js +31 -0
- package/dist/react/sections/contact-form.d.ts +16 -0
- package/dist/react/sections/contact-form.js +8 -0
- package/dist/react/sections/custom-liquid.d.ts +14 -0
- package/dist/react/sections/custom-liquid.js +7 -0
- package/dist/react/sections/custom-section.d.ts +14 -0
- package/dist/react/sections/custom-section.js +9 -0
- package/dist/react/sections/featured-collection.d.ts +10 -0
- package/dist/react/sections/featured-collection.js +23 -0
- package/dist/react/sections/hero.d.ts +6 -0
- package/dist/react/sections/hero.js +35 -0
- package/dist/react/sections/image-with-text.d.ts +13 -0
- package/dist/react/sections/image-with-text.js +9 -0
- package/dist/react/sections/multicolumn.d.ts +15 -0
- package/dist/react/sections/multicolumn.js +8 -0
- package/dist/react/sections/page.d.ts +13 -0
- package/dist/react/sections/page.js +11 -0
- package/dist/react/sections/password.d.ts +15 -0
- package/dist/react/sections/password.js +9 -0
- package/dist/react/sections/rich-text.d.ts +12 -0
- package/dist/react/sections/rich-text.js +7 -0
- package/dist/react/sections/stories.d.ts +18 -0
- package/dist/react/sections/stories.js +10 -0
- package/dist/react/sections/video-bubble.d.ts +13 -0
- package/dist/react/sections/video-bubble.js +54 -0
- package/dist/react/sections/video-carousel.d.ts +19 -0
- package/dist/react/sections/video-carousel.js +9 -0
- package/dist/react/server.d.ts +7 -0
- package/dist/react/server.js +42 -0
- package/dist/schemas/bridge.d.ts +98 -0
- package/dist/schemas/bridge.js +50 -0
- package/dist/schemas/data.d.ts +54 -0
- package/dist/schemas/data.js +22 -0
- package/dist/schemas/index.d.ts +14 -0
- package/dist/schemas/index.js +14 -0
- package/dist/schemas/manifest.d.ts +63 -0
- package/dist/schemas/manifest.js +32 -0
- package/dist/schemas/schema.d.ts +159 -0
- package/dist/schemas/schema.js +129 -0
- package/dist/schemas/validate.d.ts +22 -0
- package/dist/schemas/validate.js +199 -0
- package/package.json +84 -0
- package/src/commerce/countdown.ts +38 -0
- package/src/commerce/events.ts +43 -0
- package/src/commerce/facets.ts +88 -0
- package/src/commerce/index.ts +11 -0
- package/src/commerce/money.ts +120 -0
- package/src/commerce/product.ts +177 -0
- package/src/editor/frame.ts +305 -0
- package/src/editor/host.ts +101 -0
- package/src/editor/index.ts +9 -0
- package/src/react/adapter.tsx +83 -0
- package/src/react/blocks/_box.tsx +37 -0
- package/src/react/blocks/_slide.tsx +96 -0
- package/src/react/blocks/_video-card.tsx +131 -0
- package/src/react/blocks/accordion.tsx +33 -0
- package/src/react/blocks/button.tsx +36 -0
- package/src/react/blocks/countdown.tsx +105 -0
- package/src/react/blocks/custom-liquid.tsx +21 -0
- package/src/react/blocks/group.tsx +32 -0
- package/src/react/blocks/image.tsx +29 -0
- package/src/react/blocks/message.tsx +28 -0
- package/src/react/blocks/quote.tsx +24 -0
- package/src/react/blocks/spacer.tsx +17 -0
- package/src/react/blocks/story.tsx +40 -0
- package/src/react/blocks/text.tsx +24 -0
- package/src/react/blocks/video.tsx +132 -0
- package/src/react/components/Facets.tsx +424 -0
- package/src/react/components/Icon.tsx +96 -0
- package/src/react/components/PaymentIcons.generated.ts +24 -0
- package/src/react/components/PaymentIcons.tsx +27 -0
- package/src/react/components/Price.tsx +72 -0
- package/src/react/components/ProductBadges.tsx +95 -0
- package/src/react/components/ProductCard.tsx +417 -0
- package/src/react/components/Rating.tsx +59 -0
- package/src/react/components/SwatchStyle.tsx +50 -0
- package/src/react/components/VideoModal.tsx +236 -0
- package/src/react/engine/CssVariables.tsx +142 -0
- package/src/react/engine/context.tsx +108 -0
- package/src/react/engine/fonts.ts +42 -0
- package/src/react/engine/images.tsx +58 -0
- package/src/react/engine/render.tsx +190 -0
- package/src/react/engine/settings.ts +89 -0
- package/src/react/engine/store.ts +103 -0
- package/src/react/engine/translate.ts +40 -0
- package/src/react/engine/types.ts +142 -0
- package/src/react/index.ts +78 -0
- package/src/react/registries.server.ts +110 -0
- package/src/react/registries.ts +80 -0
- package/src/react/sections/404.tsx +37 -0
- package/src/react/sections/announcement-bar.tsx +170 -0
- package/src/react/sections/article.tsx +97 -0
- package/src/react/sections/contact-form.tsx +104 -0
- package/src/react/sections/custom-liquid.tsx +39 -0
- package/src/react/sections/custom-section.tsx +44 -0
- package/src/react/sections/featured-collection.tsx +89 -0
- package/src/react/sections/hero.tsx +84 -0
- package/src/react/sections/image-with-text.tsx +61 -0
- package/src/react/sections/multicolumn.tsx +46 -0
- package/src/react/sections/page.tsx +43 -0
- package/src/react/sections/password.tsx +60 -0
- package/src/react/sections/rich-text.tsx +37 -0
- package/src/react/sections/stories.tsx +51 -0
- package/src/react/sections/video-bubble.tsx +145 -0
- package/src/react/sections/video-carousel.tsx +57 -0
- package/src/react/server.ts +60 -0
- package/src/schemas/bridge.ts +143 -0
- package/src/schemas/data.ts +72 -0
- package/src/schemas/index.ts +14 -0
- package/src/schemas/manifest.ts +85 -0
- package/src/schemas/schema.ts +287 -0
- package/src/schemas/validate.ts +302 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in server loaders, keyed by section type (import from
|
|
3
|
+
* '@zalify/storefront-kit/react/server'). Apps spread these into installTheme()
|
|
4
|
+
* alongside their own app-local loaders.
|
|
5
|
+
*
|
|
6
|
+
* featured-collection: server loader for sections/featured-collection
|
|
7
|
+
* .liquid's data needs — the Liquid `section.settings.collection
|
|
8
|
+
* .products` lookup becomes a Storefront API query. When the collection
|
|
9
|
+
* setting is blank (fresh install), fall back to the first available
|
|
10
|
+
* collection so the section still merchandises real products; if
|
|
11
|
+
* nothing resolves the component renders its placeholder cards (the
|
|
12
|
+
* Liquid onboarding state).
|
|
13
|
+
*/
|
|
14
|
+
import {PRODUCT_CARD_FRAGMENT} from './components/ProductCard';
|
|
15
|
+
import type {SectionLoader} from './engine/types';
|
|
16
|
+
|
|
17
|
+
const FEATURED_COLLECTION_QUERY = `#graphql
|
|
18
|
+
query ThemeFeaturedCollection($handle: String!, $first: Int!) {
|
|
19
|
+
collection(handle: $handle) {
|
|
20
|
+
id
|
|
21
|
+
handle
|
|
22
|
+
title
|
|
23
|
+
products(first: $first) {
|
|
24
|
+
nodes {
|
|
25
|
+
...ProductCard
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
${PRODUCT_CARD_FRAGMENT}
|
|
31
|
+
` as const;
|
|
32
|
+
|
|
33
|
+
const FIRST_COLLECTIONS_QUERY = `#graphql
|
|
34
|
+
query ThemeFirstCollections($first: Int!) {
|
|
35
|
+
collections(first: 10) {
|
|
36
|
+
nodes {
|
|
37
|
+
id
|
|
38
|
+
handle
|
|
39
|
+
title
|
|
40
|
+
products(first: $first) {
|
|
41
|
+
nodes {
|
|
42
|
+
...ProductCard
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
${PRODUCT_CARD_FRAGMENT}
|
|
49
|
+
` as const;
|
|
50
|
+
|
|
51
|
+
const LATEST_PRODUCTS_QUERY = `#graphql
|
|
52
|
+
query ThemeLatestProducts($first: Int!) {
|
|
53
|
+
products(first: $first, sortKey: CREATED_AT, reverse: true) {
|
|
54
|
+
nodes {
|
|
55
|
+
...ProductCard
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
${PRODUCT_CARD_FRAGMENT}
|
|
60
|
+
` as const;
|
|
61
|
+
|
|
62
|
+
const featuredCollectionLoader: SectionLoader = async ({
|
|
63
|
+
settings,
|
|
64
|
+
storefront,
|
|
65
|
+
}) => {
|
|
66
|
+
const first = Number(settings.products_to_show ?? 4) || 4;
|
|
67
|
+
const handle =
|
|
68
|
+
typeof settings.collection === 'string' && settings.collection
|
|
69
|
+
? settings.collection
|
|
70
|
+
: null;
|
|
71
|
+
|
|
72
|
+
if (handle) {
|
|
73
|
+
const data = await storefront.query(FEATURED_COLLECTION_QUERY, {
|
|
74
|
+
variables: {handle, first},
|
|
75
|
+
cache: storefront.CacheLong?.(),
|
|
76
|
+
});
|
|
77
|
+
if (data?.collection) return {collection: data.collection};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Fresh-install fallback: first collection that actually has
|
|
81
|
+
// products, then the latest products as a synthetic collection —
|
|
82
|
+
// placeholders only when the store is truly empty.
|
|
83
|
+
const data = await storefront.query(FIRST_COLLECTIONS_QUERY, {
|
|
84
|
+
variables: {first},
|
|
85
|
+
cache: storefront.CacheLong?.(),
|
|
86
|
+
});
|
|
87
|
+
const nonEmpty = (data?.collections?.nodes ?? []).find(
|
|
88
|
+
(node: {products?: {nodes?: unknown[]}}) => node.products?.nodes?.length,
|
|
89
|
+
);
|
|
90
|
+
if (nonEmpty) return {collection: nonEmpty};
|
|
91
|
+
|
|
92
|
+
const latest = await storefront.query(LATEST_PRODUCTS_QUERY, {
|
|
93
|
+
variables: {first},
|
|
94
|
+
cache: storefront.CacheLong?.(),
|
|
95
|
+
});
|
|
96
|
+
const products = latest?.products?.nodes ?? [];
|
|
97
|
+
if (!products.length) return {collection: null};
|
|
98
|
+
return {
|
|
99
|
+
collection: {
|
|
100
|
+
id: 'latest',
|
|
101
|
+
handle: 'all',
|
|
102
|
+
title: 'Featured products',
|
|
103
|
+
products: {nodes: products},
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export const builtinSectionLoaders: Record<string, SectionLoader> = {
|
|
109
|
+
'featured-collection': featuredCollectionLoader,
|
|
110
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in component registries. Keys are the theme type names — the
|
|
3
|
+
* section/block `type` values in templates/*.json, which equal the old
|
|
4
|
+
* app/sections/*.tsx and app/blocks/*.tsx file names (including leading
|
|
5
|
+
* underscores and '404'). Apps spread these into installTheme() and add
|
|
6
|
+
* their own app-local sections/blocks on top.
|
|
7
|
+
*/
|
|
8
|
+
import type {BlockComponent, SectionComponent} from './engine/types';
|
|
9
|
+
|
|
10
|
+
// Blocks
|
|
11
|
+
import text from './blocks/text';
|
|
12
|
+
import button from './blocks/button';
|
|
13
|
+
import group from './blocks/group';
|
|
14
|
+
import spacer from './blocks/spacer';
|
|
15
|
+
import image from './blocks/image';
|
|
16
|
+
import box from './blocks/_box';
|
|
17
|
+
import slide from './blocks/_slide';
|
|
18
|
+
import story from './blocks/story';
|
|
19
|
+
import message from './blocks/message';
|
|
20
|
+
import quote from './blocks/quote';
|
|
21
|
+
import accordion from './blocks/accordion';
|
|
22
|
+
import countdown from './blocks/countdown';
|
|
23
|
+
import customLiquidBlock from './blocks/custom-liquid';
|
|
24
|
+
import video from './blocks/video';
|
|
25
|
+
import videoCard from './blocks/_video-card';
|
|
26
|
+
|
|
27
|
+
// Sections
|
|
28
|
+
import hero from './sections/hero';
|
|
29
|
+
import stories from './sections/stories';
|
|
30
|
+
import featuredCollection from './sections/featured-collection';
|
|
31
|
+
import multicolumn from './sections/multicolumn';
|
|
32
|
+
import imageWithText from './sections/image-with-text';
|
|
33
|
+
import richText from './sections/rich-text';
|
|
34
|
+
import customSection from './sections/custom-section';
|
|
35
|
+
import customLiquidSection from './sections/custom-liquid';
|
|
36
|
+
import announcementBar from './sections/announcement-bar';
|
|
37
|
+
import notFound from './sections/404';
|
|
38
|
+
import page from './sections/page';
|
|
39
|
+
import article from './sections/article';
|
|
40
|
+
import password from './sections/password';
|
|
41
|
+
import contactForm from './sections/contact-form';
|
|
42
|
+
import videoCarousel from './sections/video-carousel';
|
|
43
|
+
import videoBubble from './sections/video-bubble';
|
|
44
|
+
|
|
45
|
+
export const builtinSections: Record<string, SectionComponent> = {
|
|
46
|
+
hero,
|
|
47
|
+
stories,
|
|
48
|
+
'featured-collection': featuredCollection,
|
|
49
|
+
multicolumn,
|
|
50
|
+
'image-with-text': imageWithText,
|
|
51
|
+
'rich-text': richText,
|
|
52
|
+
'custom-section': customSection,
|
|
53
|
+
'custom-liquid': customLiquidSection,
|
|
54
|
+
'announcement-bar': announcementBar,
|
|
55
|
+
'404': notFound,
|
|
56
|
+
page,
|
|
57
|
+
article,
|
|
58
|
+
password,
|
|
59
|
+
'contact-form': contactForm,
|
|
60
|
+
'video-carousel': videoCarousel,
|
|
61
|
+
'video-bubble': videoBubble,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const builtinBlocks: Record<string, BlockComponent> = {
|
|
65
|
+
text,
|
|
66
|
+
button,
|
|
67
|
+
group,
|
|
68
|
+
spacer,
|
|
69
|
+
image,
|
|
70
|
+
_box: box,
|
|
71
|
+
_slide: slide,
|
|
72
|
+
story,
|
|
73
|
+
message,
|
|
74
|
+
quote,
|
|
75
|
+
accordion,
|
|
76
|
+
countdown,
|
|
77
|
+
'custom-liquid': customLiquidBlock,
|
|
78
|
+
video,
|
|
79
|
+
'_video-card': videoCard,
|
|
80
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/404.liquid. CSS: app/styles/components/sections-404.css
|
|
3
|
+
*/
|
|
4
|
+
import type {SectionProps} from '../engine/types';
|
|
5
|
+
import {t} from '../engine/translate';
|
|
6
|
+
import {useAdapter} from '../adapter';
|
|
7
|
+
|
|
8
|
+
interface NotFoundSettings {
|
|
9
|
+
color_scheme?: string;
|
|
10
|
+
section_spacing?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default function NotFoundSection({
|
|
14
|
+
settings,
|
|
15
|
+
}: SectionProps<NotFoundSettings>) {
|
|
16
|
+
const {Link} = useAdapter();
|
|
17
|
+
const {color_scheme: colorScheme = 'scheme-1', section_spacing: spacing = 'md'} =
|
|
18
|
+
settings;
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className={`not-found section color-${colorScheme} full-width`}
|
|
22
|
+
style={
|
|
23
|
+
{
|
|
24
|
+
'--section-spacing': `var(--space-section-${spacing})`,
|
|
25
|
+
} as React.CSSProperties
|
|
26
|
+
}
|
|
27
|
+
>
|
|
28
|
+
<div className="not-found__inner stack">
|
|
29
|
+
<h1 className="not-found__title">{t('404.title')}</h1>
|
|
30
|
+
<p>{t('404.not_found')}</p>
|
|
31
|
+
<Link to="/collections/all" className="button">
|
|
32
|
+
{t('404.back_to_shopping')}
|
|
33
|
+
</Link>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/announcement-bar.liquid — rotating messages bar.
|
|
3
|
+
* The Liquid theme drives rotation with the <announcement-bar> web
|
|
4
|
+
* component (src/entries/announcement.ts); here the same behavior
|
|
5
|
+
* lives in React: autoplay (paused for reduced motion), prev/next,
|
|
6
|
+
* pause/play. data-paused on the host drives the icon swap in CSS.
|
|
7
|
+
* CSS: app/styles/components/sections-announcement-bar.css
|
|
8
|
+
*/
|
|
9
|
+
import {useEffect, useRef, useState} from 'react';
|
|
10
|
+
import type {SectionProps} from '../engine/types';
|
|
11
|
+
import {t} from '../engine/translate';
|
|
12
|
+
import {Icon} from '../components/Icon';
|
|
13
|
+
|
|
14
|
+
declare global {
|
|
15
|
+
namespace JSX {
|
|
16
|
+
interface IntrinsicElements {
|
|
17
|
+
/* The extracted CSS targets the announcement-bar element itself
|
|
18
|
+
(announcement-bar[data-paused] …), so the tag name is part of
|
|
19
|
+
the styling contract and must be kept. */
|
|
20
|
+
'announcement-bar': React.DetailedHTMLProps<
|
|
21
|
+
React.HTMLAttributes<HTMLElement>,
|
|
22
|
+
HTMLElement
|
|
23
|
+
>;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface AnnouncementBarSettings {
|
|
29
|
+
rotation_speed?: number;
|
|
30
|
+
color_scheme?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/*
|
|
34
|
+
* The bar's height is a CONSTANT, published as a token so layout math
|
|
35
|
+
* (e.g. the large hero's "viewport minus header stack") can be computed
|
|
36
|
+
* in pure CSS at first paint — mirror of the section's {% style %} tag.
|
|
37
|
+
* Removing this section removes the token (consumers fall back to 0).
|
|
38
|
+
*/
|
|
39
|
+
const HEIGHT_TOKEN_CSS = `
|
|
40
|
+
:root {
|
|
41
|
+
--announcement-bar-height: calc(0.6875rem * 2.8 + 2 * var(--space-xs));
|
|
42
|
+
}
|
|
43
|
+
@media (min-width: 48rem) {
|
|
44
|
+
:root {
|
|
45
|
+
--announcement-bar-height: calc(var(--type-xs) * 2.8 + 2 * var(--space-xs));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
export default function AnnouncementBarSection({
|
|
51
|
+
section,
|
|
52
|
+
settings,
|
|
53
|
+
children,
|
|
54
|
+
}: SectionProps<AnnouncementBarSettings>) {
|
|
55
|
+
const {rotation_speed: rotationSpeed = 5, color_scheme: colorScheme = 'scheme-4'} =
|
|
56
|
+
settings;
|
|
57
|
+
const messageCount = (section.block_order ?? []).filter((key) => {
|
|
58
|
+
const block = section.blocks?.[key];
|
|
59
|
+
return block && !block.disabled;
|
|
60
|
+
}).length;
|
|
61
|
+
|
|
62
|
+
const hostRef = useRef<HTMLElement>(null);
|
|
63
|
+
const indexRef = useRef(0);
|
|
64
|
+
const timerRef = useRef<ReturnType<typeof setInterval>>();
|
|
65
|
+
const pausedRef = useRef(false);
|
|
66
|
+
const [paused, setPaused] = useState(false);
|
|
67
|
+
|
|
68
|
+
const messages = (): HTMLElement[] =>
|
|
69
|
+
Array.from(
|
|
70
|
+
hostRef.current?.querySelectorAll<HTMLElement>(
|
|
71
|
+
'.announcement-bar__message',
|
|
72
|
+
) ?? [],
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const show = (index: number) => {
|
|
76
|
+
const items = messages();
|
|
77
|
+
if (items.length < 2) return;
|
|
78
|
+
indexRef.current = (index + items.length) % items.length;
|
|
79
|
+
items.forEach((message, i) => {
|
|
80
|
+
message.hidden = i !== indexRef.current;
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const start = () => {
|
|
85
|
+
clearInterval(timerRef.current);
|
|
86
|
+
if (pausedRef.current || messageCount < 2) return;
|
|
87
|
+
const speed = rotationSpeed || 5;
|
|
88
|
+
timerRef.current = setInterval(
|
|
89
|
+
() => show(indexRef.current + 1),
|
|
90
|
+
speed * 1000,
|
|
91
|
+
);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const setPausedState = (value: boolean) => {
|
|
95
|
+
pausedRef.current = value;
|
|
96
|
+
setPaused(value);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
// Autoplay starts paused when the user prefers reduced motion.
|
|
101
|
+
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
|
102
|
+
setPausedState(true);
|
|
103
|
+
}
|
|
104
|
+
start();
|
|
105
|
+
return () => clearInterval(timerRef.current);
|
|
106
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
107
|
+
}, [messageCount, rotationSpeed]);
|
|
108
|
+
|
|
109
|
+
const previous = () => {
|
|
110
|
+
show(indexRef.current - 1);
|
|
111
|
+
start();
|
|
112
|
+
};
|
|
113
|
+
const next = () => {
|
|
114
|
+
show(indexRef.current + 1);
|
|
115
|
+
start();
|
|
116
|
+
};
|
|
117
|
+
const toggle = () => {
|
|
118
|
+
setPausedState(!pausedRef.current);
|
|
119
|
+
// pausedRef is already updated; restart (or stop) the timer.
|
|
120
|
+
start();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<announcement-bar
|
|
125
|
+
ref={hostRef}
|
|
126
|
+
className={`announcement-bar color-${colorScheme} full-width`}
|
|
127
|
+
data-speed={rotationSpeed}
|
|
128
|
+
data-paused={paused ? '' : undefined}
|
|
129
|
+
>
|
|
130
|
+
<style dangerouslySetInnerHTML={{__html: HEIGHT_TOKEN_CSS}} />
|
|
131
|
+
<div className="announcement-bar__inner">
|
|
132
|
+
<div className="announcement-bar__messages">{children}</div>
|
|
133
|
+
|
|
134
|
+
{messageCount > 1 && (
|
|
135
|
+
<div className="announcement-bar__controls">
|
|
136
|
+
<button
|
|
137
|
+
type="button"
|
|
138
|
+
className="announcement-bar__control"
|
|
139
|
+
onClick={previous}
|
|
140
|
+
aria-label={t('general.previous')}
|
|
141
|
+
>
|
|
142
|
+
<Icon name="icon-chevron-left" />
|
|
143
|
+
</button>
|
|
144
|
+
<button
|
|
145
|
+
type="button"
|
|
146
|
+
className="announcement-bar__control"
|
|
147
|
+
onClick={next}
|
|
148
|
+
aria-label={t('general.next')}
|
|
149
|
+
>
|
|
150
|
+
<Icon name="icon-chevron-right" />
|
|
151
|
+
</button>
|
|
152
|
+
<button
|
|
153
|
+
type="button"
|
|
154
|
+
className="announcement-bar__control announcement-bar__toggle"
|
|
155
|
+
onClick={toggle}
|
|
156
|
+
aria-label={t('general.pause')}
|
|
157
|
+
>
|
|
158
|
+
<span className="announcement-bar__icon-pause">
|
|
159
|
+
<Icon name="icon-pause" />
|
|
160
|
+
</span>
|
|
161
|
+
<span className="announcement-bar__icon-play">
|
|
162
|
+
<Icon name="icon-play" />
|
|
163
|
+
</span>
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
</announcement-bar>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/article.liquid. CSS: app/styles/components/sections-article.css
|
|
3
|
+
*
|
|
4
|
+
* Data: useResource('article') — the scaffold ARTICLE query shape
|
|
5
|
+
* ({handle, title, contentHtml, publishedAt, author: {name}, image}),
|
|
6
|
+
* plus useResource('blog') ({handle, title}) for the back link.
|
|
7
|
+
*
|
|
8
|
+
* The Liquid comments block (blog.comments_enabled?, {% form
|
|
9
|
+
* 'new_comment' %}) has no Storefront API mirror and is omitted.
|
|
10
|
+
*/
|
|
11
|
+
import type {SectionProps} from '../engine/types';
|
|
12
|
+
import {useResource} from '../engine/context';
|
|
13
|
+
import {t} from '../engine/translate';
|
|
14
|
+
import {imageSrcSet, imageUrl} from '../engine/images';
|
|
15
|
+
import {useAdapter} from '../adapter';
|
|
16
|
+
|
|
17
|
+
interface ArticleSettings {
|
|
18
|
+
color_scheme?: string;
|
|
19
|
+
section_spacing?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function articleDateHtml(publishedAt?: string): string {
|
|
23
|
+
if (!publishedAt) return '';
|
|
24
|
+
const formatted = new Intl.DateTimeFormat('en-US', {
|
|
25
|
+
year: 'numeric',
|
|
26
|
+
month: 'long',
|
|
27
|
+
day: 'numeric',
|
|
28
|
+
}).format(new Date(publishedAt));
|
|
29
|
+
return `<time datetime="${publishedAt}">${formatted}</time>`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default function ArticleSection({
|
|
33
|
+
settings,
|
|
34
|
+
}: SectionProps<ArticleSettings>) {
|
|
35
|
+
const {Link} = useAdapter();
|
|
36
|
+
const {color_scheme: colorScheme = 'scheme-1', section_spacing: spacing = 'md'} =
|
|
37
|
+
settings;
|
|
38
|
+
const article = useResource<any>('article');
|
|
39
|
+
const blog = useResource<any>('blog') ?? article?.blog;
|
|
40
|
+
|
|
41
|
+
if (!article) return null;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<article
|
|
45
|
+
className={`article section color-${colorScheme} full-width`}
|
|
46
|
+
style={
|
|
47
|
+
{
|
|
48
|
+
'--section-spacing': `var(--space-section-${spacing})`,
|
|
49
|
+
} as React.CSSProperties
|
|
50
|
+
}
|
|
51
|
+
>
|
|
52
|
+
<div
|
|
53
|
+
className="article__inner stack"
|
|
54
|
+
style={{'--stack-gap': 'var(--space-lg)'} as React.CSSProperties}
|
|
55
|
+
>
|
|
56
|
+
<header
|
|
57
|
+
className="article__header stack"
|
|
58
|
+
style={{'--stack-gap': 'var(--space-sm)'} as React.CSSProperties}
|
|
59
|
+
>
|
|
60
|
+
{blog?.handle && (
|
|
61
|
+
<Link className="article__back" to={`/blogs/${blog.handle}`}>
|
|
62
|
+
{t('blog.back_to_blog', {blog: blog.title ?? blog.handle})}
|
|
63
|
+
</Link>
|
|
64
|
+
)}
|
|
65
|
+
<p
|
|
66
|
+
className="article__meta"
|
|
67
|
+
dangerouslySetInnerHTML={{
|
|
68
|
+
__html: t('blog.article_metadata_html', {
|
|
69
|
+
date: articleDateHtml(article.publishedAt),
|
|
70
|
+
author: article.author?.name ?? '',
|
|
71
|
+
}),
|
|
72
|
+
}}
|
|
73
|
+
/>
|
|
74
|
+
<h1 className="article__title">{article.title}</h1>
|
|
75
|
+
</header>
|
|
76
|
+
|
|
77
|
+
{article.image?.url && (
|
|
78
|
+
<img
|
|
79
|
+
className="article__image"
|
|
80
|
+
src={imageUrl(article.image.url, 1500)}
|
|
81
|
+
srcSet={imageSrcSet(article.image.url)}
|
|
82
|
+
sizes="(min-width: 64rem) 60rem, 100vw"
|
|
83
|
+
loading="eager"
|
|
84
|
+
width={article.image.width ?? undefined}
|
|
85
|
+
height={article.image.height ?? undefined}
|
|
86
|
+
alt={article.image.altText ?? article.title ?? ''}
|
|
87
|
+
/>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<div
|
|
91
|
+
className="article__content prose"
|
|
92
|
+
dangerouslySetInnerHTML={{__html: article.contentHtml ?? ''}}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
</article>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/contact-form.liquid.
|
|
3
|
+
* CSS: app/styles/components/sections-contact-form.css
|
|
4
|
+
*
|
|
5
|
+
* Shopify's native `{% form 'contact' %}` POST endpoint isn't available
|
|
6
|
+
* in Hydrogen — the markup is rendered faithfully but submission is a
|
|
7
|
+
* no-op. TODO: wire the submit to a route action (e.g. a proxy to the
|
|
8
|
+
* Online Store contact endpoint or a third-party form service).
|
|
9
|
+
*/
|
|
10
|
+
import type {SectionProps} from '../engine/types';
|
|
11
|
+
import {t} from '../engine/translate';
|
|
12
|
+
|
|
13
|
+
interface ContactFormSettings {
|
|
14
|
+
color_scheme?: string;
|
|
15
|
+
section_spacing?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default function ContactFormSection({
|
|
19
|
+
id,
|
|
20
|
+
settings,
|
|
21
|
+
}: SectionProps<ContactFormSettings>) {
|
|
22
|
+
const {color_scheme: colorScheme = 'scheme-1', section_spacing: spacing = 'md'} =
|
|
23
|
+
settings;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className={`contact section color-${colorScheme} full-width`}
|
|
28
|
+
style={
|
|
29
|
+
{
|
|
30
|
+
'--section-spacing': `var(--space-section-${spacing})`,
|
|
31
|
+
} as React.CSSProperties
|
|
32
|
+
}
|
|
33
|
+
>
|
|
34
|
+
<div
|
|
35
|
+
className="contact__inner stack"
|
|
36
|
+
style={{'--stack-gap': 'var(--space-lg)'} as React.CSSProperties}
|
|
37
|
+
>
|
|
38
|
+
{/* TODO: POST somewhere useful — Shopify contact form posts aren't available in Hydrogen. */}
|
|
39
|
+
<form
|
|
40
|
+
className="contact__form stack"
|
|
41
|
+
method="post"
|
|
42
|
+
onSubmit={(event) => event.preventDefault()}
|
|
43
|
+
>
|
|
44
|
+
<div className="contact__row">
|
|
45
|
+
<div className="field">
|
|
46
|
+
<label className="field__label" htmlFor={`ContactName-${id}`}>
|
|
47
|
+
{t('contact.name')}
|
|
48
|
+
</label>
|
|
49
|
+
<input
|
|
50
|
+
id={`ContactName-${id}`}
|
|
51
|
+
type="text"
|
|
52
|
+
name="contact[name]"
|
|
53
|
+
autoComplete="name"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="field">
|
|
57
|
+
<label className="field__label" htmlFor={`ContactEmail-${id}`}>
|
|
58
|
+
{t('contact.email')}
|
|
59
|
+
</label>
|
|
60
|
+
<input
|
|
61
|
+
id={`ContactEmail-${id}`}
|
|
62
|
+
type="email"
|
|
63
|
+
name="contact[email]"
|
|
64
|
+
autoComplete="email"
|
|
65
|
+
spellCheck={false}
|
|
66
|
+
autoCapitalize="off"
|
|
67
|
+
required
|
|
68
|
+
/>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<div className="field">
|
|
73
|
+
<label className="field__label" htmlFor={`ContactPhone-${id}`}>
|
|
74
|
+
{t('contact.phone')}
|
|
75
|
+
</label>
|
|
76
|
+
<input
|
|
77
|
+
id={`ContactPhone-${id}`}
|
|
78
|
+
type="tel"
|
|
79
|
+
name="contact[phone]"
|
|
80
|
+
autoComplete="tel"
|
|
81
|
+
pattern="[0-9\-]*"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div className="field">
|
|
86
|
+
<label className="field__label" htmlFor={`ContactMessage-${id}`}>
|
|
87
|
+
{t('contact.message')}
|
|
88
|
+
</label>
|
|
89
|
+
<textarea
|
|
90
|
+
id={`ContactMessage-${id}`}
|
|
91
|
+
name="contact[body]"
|
|
92
|
+
rows={8}
|
|
93
|
+
required
|
|
94
|
+
/>
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
<button type="submit" className="button contact__submit">
|
|
98
|
+
{t('contact.send')}
|
|
99
|
+
</button>
|
|
100
|
+
</form>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/custom-liquid.liquid — merchant-pasted markup.
|
|
3
|
+
* The setting's raw HTML is injected as-is; any Liquid tags/objects in
|
|
4
|
+
* it cannot execute here (no Liquid runtime), so only the plain-HTML
|
|
5
|
+
* use case survives the mirror. CSS: sections-custom-liquid.css
|
|
6
|
+
*/
|
|
7
|
+
import type {SectionProps} from '../engine/types';
|
|
8
|
+
|
|
9
|
+
interface CustomLiquidSettings {
|
|
10
|
+
custom_liquid?: string;
|
|
11
|
+
color_scheme?: string;
|
|
12
|
+
section_spacing?: 'none' | 'sm' | 'md' | 'lg';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default function CustomLiquidSection({
|
|
16
|
+
settings,
|
|
17
|
+
}: SectionProps<CustomLiquidSettings>) {
|
|
18
|
+
const {
|
|
19
|
+
custom_liquid: customLiquid = '',
|
|
20
|
+
color_scheme: colorScheme = 'scheme-1',
|
|
21
|
+
section_spacing: sectionSpacing = 'md',
|
|
22
|
+
} = settings;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
className={`custom-liquid section color-${colorScheme} full-width`}
|
|
27
|
+
style={
|
|
28
|
+
{
|
|
29
|
+
'--section-spacing': `var(--space-section-${sectionSpacing})`,
|
|
30
|
+
} as React.CSSProperties
|
|
31
|
+
}
|
|
32
|
+
>
|
|
33
|
+
<div
|
|
34
|
+
className="custom-liquid__inner"
|
|
35
|
+
dangerouslySetInnerHTML={{__html: customLiquid}}
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of sections/custom-section.liquid — the canonical v3 section
|
|
3
|
+
* pattern: color_scheme + section_spacing on the wrapper, optional
|
|
4
|
+
* background image, blocks in a content grid.
|
|
5
|
+
* CSS: app/styles/components/sections-custom-section.css
|
|
6
|
+
*/
|
|
7
|
+
import type {SectionProps} from '../engine/types';
|
|
8
|
+
import {imageUrl, settingImageUrl} from '../engine/images';
|
|
9
|
+
|
|
10
|
+
interface CustomSectionSettings {
|
|
11
|
+
background_image?: string;
|
|
12
|
+
color_scheme?: string;
|
|
13
|
+
section_spacing?: 'none' | 'sm' | 'md' | 'lg';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function CustomSection({
|
|
17
|
+
settings,
|
|
18
|
+
children,
|
|
19
|
+
}: SectionProps<CustomSectionSettings>) {
|
|
20
|
+
const {
|
|
21
|
+
color_scheme: colorScheme = 'scheme-1',
|
|
22
|
+
section_spacing: sectionSpacing = 'md',
|
|
23
|
+
} = settings;
|
|
24
|
+
const backgroundImage = settingImageUrl(settings.background_image);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={`custom-section section full-width color-${colorScheme}`}
|
|
29
|
+
style={
|
|
30
|
+
{
|
|
31
|
+
'--section-spacing': `var(--space-section-${sectionSpacing})`,
|
|
32
|
+
} as React.CSSProperties
|
|
33
|
+
}
|
|
34
|
+
>
|
|
35
|
+
{backgroundImage ? (
|
|
36
|
+
<div className="custom-section__background">
|
|
37
|
+
<img src={imageUrl(backgroundImage, 2000)} alt="" />
|
|
38
|
+
</div>
|
|
39
|
+
) : null}
|
|
40
|
+
|
|
41
|
+
<div className="custom-section__content">{children}</div>
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|