create-magic-storefront 0.3.1 → 0.4.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.
Files changed (42) hide show
  1. package/README.md +18 -1
  2. package/dist/index.js +523 -9
  3. package/package.json +7 -1
  4. package/template/.claude/skills/theme-section/SKILL.md +157 -0
  5. package/template/.env.example +7 -1
  6. package/template/.screenshots/home-1280.png +0 -0
  7. package/template/.screenshots/home-390.png +0 -0
  8. package/template/.screenshots/home-filled-1280.png +0 -0
  9. package/template/.screenshots/home-filled-390.png +0 -0
  10. package/template/.screenshots/page-1280.png +0 -0
  11. package/template/AGENTS.md +54 -17
  12. package/template/PAGES.md +39 -22
  13. package/template/README.md +6 -0
  14. package/template/_gitignore +2 -0
  15. package/template/app/api/magicstore/preview/route.ts +56 -0
  16. package/template/app/globals.css +108 -0
  17. package/template/app/layout.tsx +16 -6
  18. package/template/app/page.tsx +18 -9
  19. package/template/app/pages/[handle]/page.tsx +26 -11
  20. package/template/components/sections/collections.tsx +2 -1
  21. package/template/components/sections/product-shelves.tsx +2 -1
  22. package/template/lib/api.ts +40 -10
  23. package/template/lib/i18n.ts +6 -0
  24. package/template/lib/preview.ts +21 -0
  25. package/template/llms.txt +140 -1
  26. package/template/package.json +6 -3
  27. package/template/scripts/theme.mjs +53 -0
  28. package/template/theme/index.ts +53 -0
  29. package/template/theme/sections/collection-list.tsx +64 -0
  30. package/template/theme/sections/featured-collection.tsx +56 -0
  31. package/template/theme/sections/hero.tsx +95 -0
  32. package/template/theme/sections/image-with-text.tsx +71 -0
  33. package/template/theme/sections/page-content.tsx +45 -0
  34. package/template/theme/sections/platform-home.tsx +37 -0
  35. package/template/theme/sections/product-shelf.tsx +96 -0
  36. package/template/theme/sections/rich-text.tsx +43 -0
  37. package/template/theme/sections/testimonials.tsx +84 -0
  38. package/template/theme/settings.ts +19 -0
  39. package/template/theme/templates/index.json +9 -0
  40. package/template/theme/templates/page.json +6 -0
  41. package/template/theme/text.ts +21 -0
  42. package/template/tsconfig.json +1 -1
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "sections": {
3
+ "content": { "type": "page-content", "settings": {} }
4
+ },
5
+ "order": ["content"]
6
+ }
@@ -0,0 +1,21 @@
1
+ import { MESSAGES, type Messages } from '@/lib/i18n';
2
+
3
+ /** The languages this storefront speaks; every buyer-facing default has each of them. */
4
+ export const THEME_LOCALES = ['en', 'ru', 'uz'] as const;
5
+
6
+ /** Editor-facing text (section names, labels, hints), shown in the admin's language. */
7
+ export function label(en: string, ru: string, uz: string): { en: string; ru: string; uz: string } {
8
+ return { en, ru, uz };
9
+ }
10
+
11
+ type TextKey = {
12
+ [Key in keyof Messages]: Messages[Key] extends string ? Key : never;
13
+ }[keyof Messages];
14
+
15
+ /**
16
+ * A buyer-facing default taken from `lib/i18n.ts` in every language, so the storefront's words stay
17
+ * in one place and the merchant can still change them in the admin.
18
+ */
19
+ export function message(key: TextKey): { en: string; ru: string; uz: string } {
20
+ return { en: MESSAGES.en[key], ru: MESSAGES.ru[key], uz: MESSAGES.uz[key] };
21
+ }
@@ -29,5 +29,5 @@
29
29
  ".next/types/**/*.ts",
30
30
  ".next/dev/types/**/*.ts"
31
31
  ],
32
- "exclude": ["node_modules"]
32
+ "exclude": ["node_modules", ".tmp"]
33
33
  }