create-magic-storefront 0.3.1 → 0.5.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/README.md +18 -1
- package/dist/index.js +523 -9
- package/package.json +7 -1
- package/template/.claude/skills/theme-section/SKILL.md +157 -0
- package/template/.env.example +12 -1
- package/template/.screenshots/home-1280.png +0 -0
- package/template/.screenshots/home-390.png +0 -0
- package/template/.screenshots/home-filled-1280.png +0 -0
- package/template/.screenshots/home-filled-390.png +0 -0
- package/template/.screenshots/page-1280.png +0 -0
- package/template/AGENTS.md +58 -18
- package/template/PAGES.md +48 -22
- package/template/README.md +6 -0
- package/template/_gitignore +2 -0
- package/template/app/api/magicstore/preview/route.ts +56 -0
- package/template/app/globals.css +108 -0
- package/template/app/layout.tsx +18 -7
- package/template/app/page.tsx +18 -9
- package/template/app/pages/[handle]/page.tsx +26 -11
- package/template/app/providers.tsx +15 -6
- package/template/app/storefront-api/[...path]/route.ts +11 -66
- package/template/components/sections/collections.tsx +2 -1
- package/template/components/sections/product-shelves.tsx +2 -1
- package/template/lib/api.ts +40 -10
- package/template/lib/i18n.ts +6 -0
- package/template/lib/preview.ts +21 -0
- package/template/lib/upstream.ts +21 -0
- package/template/llms.txt +153 -1
- package/template/package.json +6 -3
- package/template/scripts/theme.mjs +53 -0
- package/template/theme/index.ts +53 -0
- package/template/theme/sections/collection-list.tsx +64 -0
- package/template/theme/sections/featured-collection.tsx +56 -0
- package/template/theme/sections/hero.tsx +95 -0
- package/template/theme/sections/image-with-text.tsx +71 -0
- package/template/theme/sections/page-content.tsx +45 -0
- package/template/theme/sections/platform-home.tsx +37 -0
- package/template/theme/sections/product-shelf.tsx +96 -0
- package/template/theme/sections/rich-text.tsx +43 -0
- package/template/theme/sections/testimonials.tsx +84 -0
- package/template/theme/settings.ts +19 -0
- package/template/theme/templates/index.json +9 -0
- package/template/theme/templates/page.json +6 -0
- package/template/theme/text.ts +21 -0
- package/template/tsconfig.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { defineTheme, type SectionComponents } from '@magicstoreai/hydrogen/theme';
|
|
2
|
+
|
|
3
|
+
import { CollectionList, collectionList } from './sections/collection-list';
|
|
4
|
+
import { FeaturedCollection, featuredCollection } from './sections/featured-collection';
|
|
5
|
+
import { Hero, hero } from './sections/hero';
|
|
6
|
+
import { ImageWithText, imageWithText } from './sections/image-with-text';
|
|
7
|
+
import { pageContent } from './sections/page-content';
|
|
8
|
+
import { PlatformHome, platformHome } from './sections/platform-home';
|
|
9
|
+
import { ProductShelf, productShelf } from './sections/product-shelf';
|
|
10
|
+
import { RichText, richText } from './sections/rich-text';
|
|
11
|
+
import { Testimonials, testimonials } from './sections/testimonials';
|
|
12
|
+
import { settings } from './settings';
|
|
13
|
+
import index from './templates/index.json';
|
|
14
|
+
import page from './templates/page.json';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The storefront's theme: what `create-magic-storefront theme push` sends, so the merchant edits
|
|
18
|
+
* these sections in the admin. A new section is registered here and in `components`, then pushed
|
|
19
|
+
* (`PAGES.md`, "Creating a new section"). Bump `version` with every push of a changed theme.
|
|
20
|
+
*/
|
|
21
|
+
const theme = defineTheme({
|
|
22
|
+
name: 'starter',
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
sections: [
|
|
25
|
+
hero,
|
|
26
|
+
richText,
|
|
27
|
+
imageWithText,
|
|
28
|
+
featuredCollection,
|
|
29
|
+
collectionList,
|
|
30
|
+
testimonials,
|
|
31
|
+
productShelf,
|
|
32
|
+
pageContent,
|
|
33
|
+
platformHome,
|
|
34
|
+
],
|
|
35
|
+
settings,
|
|
36
|
+
templates: { index, page },
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
export default theme;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* One component per section type. `page-content` is bound to the page by `app/pages/[handle]`.
|
|
43
|
+
*/
|
|
44
|
+
export const components: SectionComponents<typeof theme.sections> = {
|
|
45
|
+
hero: Hero,
|
|
46
|
+
'rich-text': RichText,
|
|
47
|
+
'image-with-text': ImageWithText,
|
|
48
|
+
'featured-collection': FeaturedCollection,
|
|
49
|
+
'collection-list': CollectionList,
|
|
50
|
+
testimonials: Testimonials,
|
|
51
|
+
'product-shelf': ProductShelf,
|
|
52
|
+
'platform-home': PlatformHome,
|
|
53
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { MagicStoreError } from '@magicstoreai/storefront-client';
|
|
2
|
+
import { defineBlock, defineSection, type SectionProps } from '@magicstoreai/hydrogen/theme';
|
|
3
|
+
|
|
4
|
+
import { CollectionTiles } from '@/components/sections/collections';
|
|
5
|
+
import { api } from '@/lib/api';
|
|
6
|
+
|
|
7
|
+
import { label } from '../text';
|
|
8
|
+
|
|
9
|
+
export const collectionBlock = defineBlock({
|
|
10
|
+
type: 'collection',
|
|
11
|
+
name: label('Collection', 'Коллекция', "To'plam"),
|
|
12
|
+
settings: [
|
|
13
|
+
{ id: 'collection', type: 'COLLECTION', label: label('Collection', 'Коллекция', "To'plam") },
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const collectionList = defineSection({
|
|
18
|
+
type: 'collection-list',
|
|
19
|
+
name: label('Collections', 'Коллекции', "To'plamlar"),
|
|
20
|
+
icon: 'COLLECTIONS',
|
|
21
|
+
category: 'COLLECTIONS',
|
|
22
|
+
maxBlocks: 8,
|
|
23
|
+
settings: [
|
|
24
|
+
{ id: 'heading', type: 'TEXT', label: label('Heading', 'Заголовок', 'Sarlavha'), default: '' },
|
|
25
|
+
],
|
|
26
|
+
blocks: [collectionBlock],
|
|
27
|
+
presets: [
|
|
28
|
+
{
|
|
29
|
+
name: label('Collections', 'Коллекции', "To'plamlar"),
|
|
30
|
+
blocks: [{ type: 'collection' }, { type: 'collection' }, { type: 'collection' }],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
/** Tiles of the collections the merchant picked, in their order. */
|
|
36
|
+
export async function CollectionList({ settings, blocks }: SectionProps<typeof collectionList>) {
|
|
37
|
+
const handles = blocks.flatMap((block) =>
|
|
38
|
+
block.settings.collection ? [block.settings.collection.handle] : [],
|
|
39
|
+
);
|
|
40
|
+
const collections = await Promise.all(
|
|
41
|
+
handles.map((handle) =>
|
|
42
|
+
api.collectionsShow({ path: { handle } }).then(
|
|
43
|
+
({ data }) => data,
|
|
44
|
+
(error: unknown) => {
|
|
45
|
+
// Unpublished between the template read and this one: leave the tile out.
|
|
46
|
+
if (error instanceof MagicStoreError && error.code === 'NOT_FOUND') {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
throw error;
|
|
50
|
+
},
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
const found = collections.filter((collection) => collection !== null);
|
|
55
|
+
if (found.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return (
|
|
59
|
+
<section className="stack">
|
|
60
|
+
{settings.heading && <h2>{settings.heading}</h2>}
|
|
61
|
+
<CollectionTiles collections={found} />
|
|
62
|
+
</section>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { defineSection, type SectionProps } from '@magicstoreai/hydrogen/theme';
|
|
2
|
+
|
|
3
|
+
import { Shelf } from '@/components/sections/product-shelves';
|
|
4
|
+
import { api } from '@/lib/api';
|
|
5
|
+
|
|
6
|
+
import { label } from '../text';
|
|
7
|
+
|
|
8
|
+
export const featuredCollection = defineSection({
|
|
9
|
+
type: 'featured-collection',
|
|
10
|
+
name: label('Featured collection', 'Подборка из коллекции', "To'plamdan tanlov"),
|
|
11
|
+
icon: 'PRODUCTS',
|
|
12
|
+
category: 'PRODUCTS',
|
|
13
|
+
settings: [
|
|
14
|
+
{ id: 'collection', type: 'COLLECTION', label: label('Collection', 'Коллекция', "To'plam") },
|
|
15
|
+
{
|
|
16
|
+
id: 'heading',
|
|
17
|
+
type: 'TEXT',
|
|
18
|
+
label: label('Heading', 'Заголовок', 'Sarlavha'),
|
|
19
|
+
info: label(
|
|
20
|
+
'Empty: the collection title.',
|
|
21
|
+
'Пусто — название коллекции.',
|
|
22
|
+
"Bo'sh — to'plam nomi.",
|
|
23
|
+
),
|
|
24
|
+
default: '',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'limit',
|
|
28
|
+
type: 'RANGE',
|
|
29
|
+
label: label('Products', 'Товаров', 'Mahsulotlar'),
|
|
30
|
+
min: 2,
|
|
31
|
+
max: 24,
|
|
32
|
+
step: 1,
|
|
33
|
+
default: 8,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
presets: [{ name: label('Featured collection', 'Подборка из коллекции', "To'plamdan tanlov") }],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
/** Products of the collection the merchant picked; nothing until one is picked. */
|
|
40
|
+
export async function FeaturedCollection({ settings }: SectionProps<typeof featuredCollection>) {
|
|
41
|
+
const { collection } = settings;
|
|
42
|
+
if (collection === null) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const { data } = await api.collectionsProducts({
|
|
46
|
+
path: { handle: collection.handle },
|
|
47
|
+
query: { perPage: settings.limit },
|
|
48
|
+
});
|
|
49
|
+
return (
|
|
50
|
+
<Shelf
|
|
51
|
+
title={settings.heading || collection.title}
|
|
52
|
+
href={`/collections/${collection.handle}`}
|
|
53
|
+
products={data}
|
|
54
|
+
/>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Image } from '@magicstoreai/hydrogen';
|
|
2
|
+
import { defineSection, type SectionProps } from '@magicstoreai/hydrogen/theme';
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
|
|
5
|
+
import { api } from '@/lib/api';
|
|
6
|
+
|
|
7
|
+
import { label, message } from '../text';
|
|
8
|
+
|
|
9
|
+
export const hero = defineSection({
|
|
10
|
+
type: 'hero',
|
|
11
|
+
name: label('Hero', 'Главный баннер', 'Asosiy banner'),
|
|
12
|
+
icon: 'IMAGE',
|
|
13
|
+
category: 'MEDIA',
|
|
14
|
+
limit: 1,
|
|
15
|
+
settings: [
|
|
16
|
+
{
|
|
17
|
+
id: 'heading',
|
|
18
|
+
type: 'TEXT',
|
|
19
|
+
label: label('Heading', 'Заголовок', 'Sarlavha'),
|
|
20
|
+
info: label('Empty: the shop name.', 'Пусто — название магазина.', "Bo'sh — do'kon nomi."),
|
|
21
|
+
maxLength: 120,
|
|
22
|
+
default: '',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'subheading',
|
|
26
|
+
type: 'TEXTAREA',
|
|
27
|
+
label: label('Subheading', 'Подзаголовок', 'Kichik sarlavha'),
|
|
28
|
+
info: label(
|
|
29
|
+
'Empty: the shop description.',
|
|
30
|
+
'Пусто — описание магазина.',
|
|
31
|
+
"Bo'sh — do'kon tavsifi.",
|
|
32
|
+
),
|
|
33
|
+
maxLength: 400,
|
|
34
|
+
default: '',
|
|
35
|
+
},
|
|
36
|
+
{ id: 'image', type: 'IMAGE', label: label('Image', 'Изображение', 'Rasm') },
|
|
37
|
+
{
|
|
38
|
+
id: 'imageAlt',
|
|
39
|
+
type: 'TEXT',
|
|
40
|
+
label: label('Image description', 'Описание изображения', 'Rasm tavsifi'),
|
|
41
|
+
info: label(
|
|
42
|
+
'For screen readers. Empty: decorative.',
|
|
43
|
+
'Для экранных дикторов. Пусто — декоративное.',
|
|
44
|
+
"Ekran o'quvchilari uchun. Bo'sh — bezak.",
|
|
45
|
+
),
|
|
46
|
+
default: '',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'buttonLabel',
|
|
50
|
+
type: 'TEXT',
|
|
51
|
+
label: label('Button text', 'Текст кнопки', 'Tugma matni'),
|
|
52
|
+
info: label('Empty: no button.', 'Пусто — без кнопки.', "Bo'sh — tugmasiz."),
|
|
53
|
+
maxLength: 40,
|
|
54
|
+
default: message('shopNow'),
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: 'buttonLink',
|
|
58
|
+
type: 'URL',
|
|
59
|
+
label: label('Button link', 'Ссылка кнопки', 'Tugma havolasi'),
|
|
60
|
+
default: '/search',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'alignment',
|
|
64
|
+
type: 'SELECT',
|
|
65
|
+
label: label('Alignment', 'Выравнивание', 'Tekislash'),
|
|
66
|
+
default: 'CENTER',
|
|
67
|
+
options: [
|
|
68
|
+
{ value: 'LEFT', label: label('Left', 'Слева', 'Chapda') },
|
|
69
|
+
{ value: 'CENTER', label: label('Center', 'По центру', 'Markazda') },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
presets: [{ name: label('Hero', 'Главный баннер', 'Asosiy banner') }],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
/** The page's opening: the merchant's heading over their image, the shop's name when left empty. */
|
|
77
|
+
export async function Hero({ settings }: SectionProps<typeof hero>) {
|
|
78
|
+
const { data: shop } = await api.shop();
|
|
79
|
+
const heading = settings.heading || shop.name;
|
|
80
|
+
const subheading = settings.subheading || shop.description;
|
|
81
|
+
return (
|
|
82
|
+
<section className={`hero hero-${settings.alignment.toLowerCase()}`}>
|
|
83
|
+
{settings.image && <Image data={settings.image} alt={settings.imageAlt} loading="eager" />}
|
|
84
|
+
<div className="hero-text stack">
|
|
85
|
+
<h1>{heading}</h1>
|
|
86
|
+
{subheading && <p>{subheading}</p>}
|
|
87
|
+
{settings.buttonLabel && settings.buttonLink && (
|
|
88
|
+
<Link href={settings.buttonLink} className="button primary">
|
|
89
|
+
{settings.buttonLabel}
|
|
90
|
+
</Link>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
</section>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Image } from '@magicstoreai/hydrogen';
|
|
2
|
+
import { defineSection, type SectionProps } from '@magicstoreai/hydrogen/theme';
|
|
3
|
+
import Link from 'next/link';
|
|
4
|
+
|
|
5
|
+
import { label } from '../text';
|
|
6
|
+
|
|
7
|
+
export const imageWithText = defineSection({
|
|
8
|
+
type: 'image-with-text',
|
|
9
|
+
name: label('Image with text', 'Изображение с текстом', 'Rasm va matn'),
|
|
10
|
+
icon: 'IMAGE',
|
|
11
|
+
category: 'MEDIA',
|
|
12
|
+
settings: [
|
|
13
|
+
{ id: 'image', type: 'IMAGE', label: label('Image', 'Изображение', 'Rasm') },
|
|
14
|
+
{
|
|
15
|
+
id: 'imageAlt',
|
|
16
|
+
type: 'TEXT',
|
|
17
|
+
label: label('Image description', 'Описание изображения', 'Rasm tavsifi'),
|
|
18
|
+
default: '',
|
|
19
|
+
},
|
|
20
|
+
{ id: 'heading', type: 'TEXT', label: label('Heading', 'Заголовок', 'Sarlavha'), default: '' },
|
|
21
|
+
{ id: 'body', type: 'RICHTEXT', label: label('Text', 'Текст', 'Matn'), default: '' },
|
|
22
|
+
{
|
|
23
|
+
id: 'buttonLabel',
|
|
24
|
+
type: 'TEXT',
|
|
25
|
+
label: label('Button text', 'Текст кнопки', 'Tugma matni'),
|
|
26
|
+
info: label('Empty: no button.', 'Пусто — без кнопки.', "Bo'sh — tugmasiz."),
|
|
27
|
+
default: '',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'buttonLink',
|
|
31
|
+
type: 'URL',
|
|
32
|
+
label: label('Button link', 'Ссылка кнопки', 'Tugma havolasi'),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
id: 'layout',
|
|
36
|
+
type: 'SELECT',
|
|
37
|
+
label: label('Image position', 'Положение изображения', 'Rasm joylashuvi'),
|
|
38
|
+
default: 'IMAGE_LEFT',
|
|
39
|
+
options: [
|
|
40
|
+
{ value: 'IMAGE_LEFT', label: label('Left', 'Слева', 'Chapda') },
|
|
41
|
+
{ value: 'IMAGE_RIGHT', label: label('Right', 'Справа', "O'ngda") },
|
|
42
|
+
],
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
presets: [{ name: label('Image with text', 'Изображение с текстом', 'Rasm va matn') }],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** An image beside the merchant's text; renders nothing until the merchant fills it in. */
|
|
49
|
+
export function ImageWithText({ settings }: SectionProps<typeof imageWithText>) {
|
|
50
|
+
if (!settings.image && !settings.heading && !settings.body) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
return (
|
|
54
|
+
<section
|
|
55
|
+
className={`image-with-text ${settings.layout === 'IMAGE_RIGHT' ? 'image-right' : ''}`}
|
|
56
|
+
>
|
|
57
|
+
{settings.image && <Image data={settings.image} alt={settings.imageAlt} />}
|
|
58
|
+
<div className="stack">
|
|
59
|
+
{settings.heading && <h2>{settings.heading}</h2>}
|
|
60
|
+
{settings.body && (
|
|
61
|
+
<div className="prose" dangerouslySetInnerHTML={{ __html: settings.body }} />
|
|
62
|
+
)}
|
|
63
|
+
{settings.buttonLabel && settings.buttonLink && (
|
|
64
|
+
<Link href={settings.buttonLink} className="button">
|
|
65
|
+
{settings.buttonLabel}
|
|
66
|
+
</Link>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
</section>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Schema } from '@magicstoreai/storefront-client';
|
|
2
|
+
import { defineSection, type SectionProps } from '@magicstoreai/hydrogen/theme';
|
|
3
|
+
|
|
4
|
+
import { locale } from '@/lib/api';
|
|
5
|
+
import { messages } from '@/lib/i18n';
|
|
6
|
+
|
|
7
|
+
import { label } from '../text';
|
|
8
|
+
|
|
9
|
+
export const pageContent = defineSection({
|
|
10
|
+
type: 'page-content',
|
|
11
|
+
name: label('Page content', 'Содержимое страницы', 'Sahifa matni'),
|
|
12
|
+
icon: 'TEXT',
|
|
13
|
+
category: 'TEXT',
|
|
14
|
+
enabledOn: ['page'],
|
|
15
|
+
limit: 1,
|
|
16
|
+
settings: [
|
|
17
|
+
{
|
|
18
|
+
id: 'showTitle',
|
|
19
|
+
type: 'CHECKBOX',
|
|
20
|
+
label: label('Show the title', 'Показывать заголовок', "Sarlavhani ko'rsatish"),
|
|
21
|
+
default: true,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
presets: [{ name: label('Page content', 'Содержимое страницы', 'Sahifa matni') }],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The content page's own title and body (`pagesShow`). `app/pages/[handle]` binds the page; `body`
|
|
29
|
+
* is the merchant's HTML from the pages editor.
|
|
30
|
+
*/
|
|
31
|
+
export function PageContent({
|
|
32
|
+
page,
|
|
33
|
+
settings,
|
|
34
|
+
}: SectionProps<typeof pageContent> & { page: Schema<'PageDetail'> }) {
|
|
35
|
+
return (
|
|
36
|
+
<article className="stack">
|
|
37
|
+
{settings.showTitle && <h1>{page.title}</h1>}
|
|
38
|
+
{page.body ? (
|
|
39
|
+
<div dangerouslySetInnerHTML={{ __html: page.body }} />
|
|
40
|
+
) : (
|
|
41
|
+
<p className="muted">{messages(locale).nothingHere}</p>
|
|
42
|
+
)}
|
|
43
|
+
</article>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineSection } from '@magicstoreai/hydrogen/theme';
|
|
2
|
+
|
|
3
|
+
import { HomeSections } from '@/components/sections';
|
|
4
|
+
import { api } from '@/lib/api';
|
|
5
|
+
|
|
6
|
+
import { label } from '../text';
|
|
7
|
+
|
|
8
|
+
export const platformHome = defineSection({
|
|
9
|
+
type: 'platform-home',
|
|
10
|
+
name: label(
|
|
11
|
+
'Platform home sections',
|
|
12
|
+
'Секции главной из админки',
|
|
13
|
+
"Admin paneldagi bosh sahifa bo'limlari",
|
|
14
|
+
),
|
|
15
|
+
icon: 'LAYOUT',
|
|
16
|
+
category: 'OTHER',
|
|
17
|
+
enabledOn: ['index'],
|
|
18
|
+
limit: 1,
|
|
19
|
+
presets: [
|
|
20
|
+
{
|
|
21
|
+
name: label(
|
|
22
|
+
'Platform home sections',
|
|
23
|
+
'Секции главной из админки',
|
|
24
|
+
"Admin paneldagi bosh sahifa bo'limlari",
|
|
25
|
+
),
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The layout of the platform's home editor (`GET /home`: banners, stories, deal of the day…),
|
|
32
|
+
* rendered by `components/sections`. Optional: the default template does not include it.
|
|
33
|
+
*/
|
|
34
|
+
export async function PlatformHome() {
|
|
35
|
+
const { data: home } = await api.home();
|
|
36
|
+
return <HomeSections sections={home.sections} />;
|
|
37
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { defineSection, type SectionProps } from '@magicstoreai/hydrogen/theme';
|
|
2
|
+
|
|
3
|
+
import { Shelf } from '@/components/sections/product-shelves';
|
|
4
|
+
import { api, locale } from '@/lib/api';
|
|
5
|
+
import { messages } from '@/lib/i18n';
|
|
6
|
+
|
|
7
|
+
import { label } from '../text';
|
|
8
|
+
|
|
9
|
+
export const productShelf = defineSection({
|
|
10
|
+
type: 'product-shelf',
|
|
11
|
+
name: label('Product shelf', 'Полка товаров', 'Mahsulotlar qatori'),
|
|
12
|
+
icon: 'PRODUCTS',
|
|
13
|
+
category: 'PRODUCTS',
|
|
14
|
+
settings: [
|
|
15
|
+
{
|
|
16
|
+
id: 'source',
|
|
17
|
+
type: 'SELECT',
|
|
18
|
+
label: label('Products', 'Товары', 'Mahsulotlar'),
|
|
19
|
+
default: 'NEW_ARRIVALS',
|
|
20
|
+
options: [
|
|
21
|
+
{ value: 'NEW_ARRIVALS', label: label('New arrivals', 'Новинки', 'Yangi kelganlar') },
|
|
22
|
+
{ value: 'ON_SALE', label: label('On sale', 'Со скидкой', 'Chegirmada') },
|
|
23
|
+
{ value: 'BESTSELLERS', label: label('Bestsellers', 'Хиты продаж', "Eng ko'p sotilgan") },
|
|
24
|
+
{ value: 'COLLECTION', label: label('From a collection', 'Из коллекции', "To'plamdan") },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'collection',
|
|
29
|
+
type: 'COLLECTION',
|
|
30
|
+
label: label('Collection', 'Коллекция', "To'plam"),
|
|
31
|
+
info: label(
|
|
32
|
+
'Used when the products come from a collection.',
|
|
33
|
+
'Для товаров из коллекции.',
|
|
34
|
+
"Mahsulotlar to'plamdan bo'lsa.",
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: 'heading',
|
|
39
|
+
type: 'TEXT',
|
|
40
|
+
label: label('Heading', 'Заголовок', 'Sarlavha'),
|
|
41
|
+
info: label(
|
|
42
|
+
'Empty: named after the products.',
|
|
43
|
+
'Пусто — по типу товаров.',
|
|
44
|
+
"Bo'sh — avtomatik.",
|
|
45
|
+
),
|
|
46
|
+
default: '',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'limit',
|
|
50
|
+
type: 'RANGE',
|
|
51
|
+
label: label('How many', 'Сколько', 'Nechta'),
|
|
52
|
+
min: 2,
|
|
53
|
+
max: 24,
|
|
54
|
+
step: 1,
|
|
55
|
+
default: 8,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
presets: [{ name: label('Product shelf', 'Полка товаров', 'Mahsulotlar qatori') }],
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
/** A row of products through the catalog endpoints: no `GET /home` needed. */
|
|
62
|
+
export async function ProductShelf({ settings }: SectionProps<typeof productShelf>) {
|
|
63
|
+
const t = messages(locale);
|
|
64
|
+
const perPage = settings.limit;
|
|
65
|
+
switch (settings.source) {
|
|
66
|
+
case 'NEW_ARRIVALS': {
|
|
67
|
+
const { data } = await api.productsIndex({ query: { sort: '-createdAt', perPage } });
|
|
68
|
+
return <Shelf title={settings.heading || t.newArrivals} products={data} />;
|
|
69
|
+
}
|
|
70
|
+
case 'ON_SALE': {
|
|
71
|
+
const { data } = await api.productsIndex({ query: { 'filter[onSale]': 'true', perPage } });
|
|
72
|
+
return <Shelf title={settings.heading || t.onSale} products={data} />;
|
|
73
|
+
}
|
|
74
|
+
case 'BESTSELLERS': {
|
|
75
|
+
const { data } = await api.productsIndex({ query: { sort: '-bestSellers', perPage } });
|
|
76
|
+
return <Shelf title={settings.heading || t.bestsellers} products={data} />;
|
|
77
|
+
}
|
|
78
|
+
case 'COLLECTION': {
|
|
79
|
+
const { collection } = settings;
|
|
80
|
+
if (collection === null) {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const { data } = await api.collectionsProducts({
|
|
84
|
+
path: { handle: collection.handle },
|
|
85
|
+
query: { perPage },
|
|
86
|
+
});
|
|
87
|
+
return (
|
|
88
|
+
<Shelf
|
|
89
|
+
title={settings.heading || collection.title}
|
|
90
|
+
href={`/collections/${collection.handle}`}
|
|
91
|
+
products={data}
|
|
92
|
+
/>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { defineSection, type SectionProps } from '@magicstoreai/hydrogen/theme';
|
|
2
|
+
|
|
3
|
+
import { label } from '../text';
|
|
4
|
+
|
|
5
|
+
export const richText = defineSection({
|
|
6
|
+
type: 'rich-text',
|
|
7
|
+
name: label('Text', 'Текст', 'Matn'),
|
|
8
|
+
icon: 'TEXT',
|
|
9
|
+
category: 'TEXT',
|
|
10
|
+
settings: [
|
|
11
|
+
{ id: 'heading', type: 'TEXT', label: label('Heading', 'Заголовок', 'Sarlavha'), default: '' },
|
|
12
|
+
{ id: 'content', type: 'RICHTEXT', label: label('Text', 'Текст', 'Matn'), default: '' },
|
|
13
|
+
{
|
|
14
|
+
id: 'width',
|
|
15
|
+
type: 'SELECT',
|
|
16
|
+
label: label('Width', 'Ширина', 'Kenglik'),
|
|
17
|
+
default: 'NARROW',
|
|
18
|
+
options: [
|
|
19
|
+
{ value: 'NARROW', label: label('Narrow', 'Узкая', 'Tor') },
|
|
20
|
+
{ value: 'WIDE', label: label('Wide', 'Широкая', 'Keng') },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
presets: [{ name: label('Text', 'Текст', 'Matn') }],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The merchant's formatted text. `content` is RICHTEXT, which the API sanitises to
|
|
29
|
+
* `p, br, strong, em, a, ul, ol, li`, so it is the one setting rendered as HTML.
|
|
30
|
+
*/
|
|
31
|
+
export function RichText({ settings }: SectionProps<typeof richText>) {
|
|
32
|
+
if (!settings.heading && !settings.content) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
return (
|
|
36
|
+
<section className={`stack rich-text rich-text-${settings.width.toLowerCase()}`}>
|
|
37
|
+
{settings.heading && <h2>{settings.heading}</h2>}
|
|
38
|
+
{settings.content && (
|
|
39
|
+
<div className="prose" dangerouslySetInnerHTML={{ __html: settings.content }} />
|
|
40
|
+
)}
|
|
41
|
+
</section>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { defineBlock, defineSection, type SectionProps } from '@magicstoreai/hydrogen/theme';
|
|
2
|
+
|
|
3
|
+
import { locale } from '@/lib/api';
|
|
4
|
+
import { messages } from '@/lib/i18n';
|
|
5
|
+
|
|
6
|
+
import { label } from '../text';
|
|
7
|
+
|
|
8
|
+
export const quoteBlock = defineBlock({
|
|
9
|
+
type: 'quote',
|
|
10
|
+
name: label('Quote', 'Отзыв', 'Fikr'),
|
|
11
|
+
settings: [
|
|
12
|
+
{
|
|
13
|
+
id: 'text',
|
|
14
|
+
type: 'TEXTAREA',
|
|
15
|
+
label: label('Quote', 'Отзыв', 'Fikr'),
|
|
16
|
+
maxLength: 600,
|
|
17
|
+
default: '',
|
|
18
|
+
},
|
|
19
|
+
{ id: 'author', type: 'TEXT', label: label('Author', 'Автор', 'Muallif'), default: '' },
|
|
20
|
+
{
|
|
21
|
+
id: 'rating',
|
|
22
|
+
type: 'RANGE',
|
|
23
|
+
label: label('Rating', 'Оценка', 'Baho'),
|
|
24
|
+
min: 1,
|
|
25
|
+
max: 5,
|
|
26
|
+
step: 1,
|
|
27
|
+
default: 5,
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const testimonials = defineSection({
|
|
33
|
+
type: 'testimonials',
|
|
34
|
+
name: label('Testimonials', 'Отзывы покупателей', 'Xaridorlar fikri'),
|
|
35
|
+
icon: 'QUOTE',
|
|
36
|
+
category: 'SOCIAL_PROOF',
|
|
37
|
+
maxBlocks: 12,
|
|
38
|
+
settings: [
|
|
39
|
+
{
|
|
40
|
+
id: 'heading',
|
|
41
|
+
type: 'TEXT',
|
|
42
|
+
label: label('Heading', 'Заголовок', 'Sarlavha'),
|
|
43
|
+
info: label(
|
|
44
|
+
'Empty: "What our customers say".',
|
|
45
|
+
'Пусто — «Что говорят покупатели».',
|
|
46
|
+
"Bo'sh — standart sarlavha.",
|
|
47
|
+
),
|
|
48
|
+
default: '',
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
blocks: [quoteBlock],
|
|
52
|
+
presets: [
|
|
53
|
+
{
|
|
54
|
+
name: label('Testimonials', 'Отзывы покупателей', 'Xaridorlar fikri'),
|
|
55
|
+
blocks: [{ type: 'quote' }, { type: 'quote' }],
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/** Quotes the merchant entered from real customers; a quote without text is left out. */
|
|
61
|
+
export function Testimonials({ settings, blocks }: SectionProps<typeof testimonials>) {
|
|
62
|
+
const t = messages(locale);
|
|
63
|
+
const quotes = blocks.filter((block) => block.settings.text !== '');
|
|
64
|
+
if (quotes.length === 0) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
return (
|
|
68
|
+
<section className="stack">
|
|
69
|
+
<h2>{settings.heading || t.customersSay}</h2>
|
|
70
|
+
<div className="testimonials">
|
|
71
|
+
{quotes.map(({ id, settings: quote }) => (
|
|
72
|
+
<figure key={id} className="stack">
|
|
73
|
+
<span className="rating" role="img" aria-label={t.ratedOutOf(quote.rating)}>
|
|
74
|
+
{'★'.repeat(quote.rating)}
|
|
75
|
+
{'☆'.repeat(5 - quote.rating)}
|
|
76
|
+
</span>
|
|
77
|
+
<blockquote>{quote.text}</blockquote>
|
|
78
|
+
{quote.author && <figcaption className="muted">{quote.author}</figcaption>}
|
|
79
|
+
</figure>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</section>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineThemeSettings } from '@magicstoreai/hydrogen/theme';
|
|
2
|
+
|
|
3
|
+
import { label } from './text';
|
|
4
|
+
|
|
5
|
+
/** Global settings, edited in the admin under the theme's settings. Brand colors stay in `shop.branding`. */
|
|
6
|
+
export const settings = defineThemeSettings([
|
|
7
|
+
{
|
|
8
|
+
id: 'announcement',
|
|
9
|
+
type: 'TEXT',
|
|
10
|
+
label: label('Announcement', 'Объявление', "E'lon"),
|
|
11
|
+
info: label(
|
|
12
|
+
'A line above the header on every page. Empty: none.',
|
|
13
|
+
'Строка над шапкой на каждой странице. Пусто — не показывается.',
|
|
14
|
+
"Har bir sahifada sarlavha ustidagi qator. Bo'sh — ko'rsatilmaydi.",
|
|
15
|
+
),
|
|
16
|
+
maxLength: 200,
|
|
17
|
+
default: '',
|
|
18
|
+
},
|
|
19
|
+
]);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"sections": {
|
|
3
|
+
"hero": { "type": "hero", "settings": {} },
|
|
4
|
+
"featured": { "type": "featured-collection", "settings": {} },
|
|
5
|
+
"new-arrivals": { "type": "product-shelf", "settings": { "source": "NEW_ARRIVALS" } },
|
|
6
|
+
"story": { "type": "image-with-text", "settings": {} }
|
|
7
|
+
},
|
|
8
|
+
"order": ["hero", "featured", "new-arrivals", "story"]
|
|
9
|
+
}
|