@jarwizz/create-jarshop 0.1.1

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +25 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +14 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/generator.d.ts +16 -0
  7. package/dist/generator.js +198 -0
  8. package/dist/generator.js.map +1 -0
  9. package/dist/template/AGENTS.md +41 -0
  10. package/dist/template/CONTEXT.md +37 -0
  11. package/dist/template/MEMORY.md +16 -0
  12. package/dist/template/README.md +63 -0
  13. package/dist/template/docker-compose.yml +14 -0
  14. package/dist/template/env.example.template +14 -0
  15. package/dist/template/gitignore.template +10 -0
  16. package/dist/template/package.json +46 -0
  17. package/dist/template/pnpm-lock.yaml +12852 -0
  18. package/dist/template/pnpm-workspace.yaml +24 -0
  19. package/dist/template/src/dev.ts +154 -0
  20. package/dist/template/src/environment.d.ts +19 -0
  21. package/dist/template/src/index-worker.ts +9 -0
  22. package/dist/template/src/index.ts +7 -0
  23. package/dist/template/src/migrations/1786024108433-initial.ts +2677 -0
  24. package/dist/template/src/seed.ts +138 -0
  25. package/dist/template/src/vendure-config.ts +75 -0
  26. package/dist/template/static/email/templates/email-address-change/body.hbs +20 -0
  27. package/dist/template/static/email/templates/email-verification/body.hbs +20 -0
  28. package/dist/template/static/email/templates/order-confirmation/body.hbs +133 -0
  29. package/dist/template/static/email/templates/partials/footer.hbs +10 -0
  30. package/dist/template/static/email/templates/partials/header.hbs +18 -0
  31. package/dist/template/static/email/templates/password-reset/body.hbs +24 -0
  32. package/dist/template/storefront/README.md +8 -0
  33. package/dist/template/storefront/app/[locale]/[slug]/page.tsx +52 -0
  34. package/dist/template/storefront/app/[locale]/page.tsx +35 -0
  35. package/dist/template/storefront/app/globals.css +16 -0
  36. package/dist/template/storefront/eslint.config.mjs +9 -0
  37. package/dist/template/storefront/lib/commerce.ts +35 -0
  38. package/dist/template/storefront/next-env.d.ts +6 -0
  39. package/dist/template/storefront/next.config.ts +14 -0
  40. package/dist/template/storefront/package.json +32 -0
  41. package/dist/template/storefront/pnpm-lock.yaml +3562 -0
  42. package/dist/template/storefront/pnpm-workspace.yaml +26 -0
  43. package/dist/template/storefront/tsconfig.json +30 -0
  44. package/dist/template/tsconfig.dashboard.json +4 -0
  45. package/dist/template/tsconfig.json +15 -0
  46. package/dist/template/turbo.json +8 -0
  47. package/dist/template/vite.config.mts +26 -0
  48. package/dist/template-manifest.json +3 -0
  49. package/dist/template.d.ts +8 -0
  50. package/dist/template.js +54 -0
  51. package/dist/template.js.map +1 -0
  52. package/package.json +50 -0
@@ -0,0 +1,138 @@
1
+ import {
2
+ CurrencyCode,
3
+ LanguageCode,
4
+ ProductService,
5
+ ProductVariantService,
6
+ RequestContextService,
7
+ StockLocationService,
8
+ TaxCategoryService,
9
+ TaxRateService,
10
+ ZoneService,
11
+ ChannelService,
12
+ bootstrap,
13
+ } from "@vendure/core";
14
+ import { GlobalFlag } from "@vendure/common/lib/generated-types";
15
+ import { config } from "./vendure-config";
16
+
17
+ const productSlug = "jarshop-demo-product";
18
+
19
+ async function seed(): Promise<void> {
20
+ if (!new Set(["dev", "test"]).has(process.env.APP_ENV ?? "")) {
21
+ throw new Error(
22
+ "JarShop demo seed is allowed only in APP_ENV=dev or test.",
23
+ );
24
+ }
25
+ const app = await bootstrap(config);
26
+
27
+ try {
28
+ const requestContextService = app.get(RequestContextService);
29
+ let ctx = await requestContextService.create({ apiType: "admin" });
30
+ const productService = app.get(ProductService);
31
+ const variantService = app.get(ProductVariantService);
32
+ const stockLocationService = app.get(StockLocationService);
33
+ const zoneService = app.get(ZoneService);
34
+ const taxCategoryService = app.get(TaxCategoryService);
35
+ const taxRateService = app.get(TaxRateService);
36
+ const channelService = app.get(ChannelService);
37
+ const existing = await productService.findOneBySlug(ctx, productSlug);
38
+
39
+ const stockLocation = await stockLocationService.defaultStockLocation(ctx);
40
+ let channel = await channelService.getDefaultChannel(ctx);
41
+ const zones = await zoneService.findAll(ctx);
42
+ const taxZone =
43
+ zones.items[0] ??
44
+ (await zoneService.create(ctx, { name: "JarShop default tax zone" }));
45
+ if (
46
+ !channel.defaultTaxZone ||
47
+ channel.defaultCurrencyCode !== CurrencyCode.EUR
48
+ ) {
49
+ await channelService.update(ctx, {
50
+ id: channel.id,
51
+ defaultTaxZoneId: taxZone.id,
52
+ defaultCurrencyCode: CurrencyCode.EUR,
53
+ availableCurrencyCodes: [CurrencyCode.EUR],
54
+ });
55
+ channel = await channelService.getDefaultChannel(ctx);
56
+ ctx = await requestContextService.create({
57
+ apiType: "admin",
58
+ channelOrToken: channel.token,
59
+ });
60
+ }
61
+ const taxCategories = await taxCategoryService.findAll(ctx);
62
+ const taxCategory =
63
+ taxCategories.items.find((item) => item.isDefault) ??
64
+ taxCategories.items[0] ??
65
+ (await taxCategoryService.create(ctx, { name: "JarShop demo tax" }));
66
+ const taxRates = await taxRateService.findAll(ctx);
67
+ if (
68
+ !taxRates.items.some(
69
+ (rate) =>
70
+ rate.zoneId === taxZone.id && rate.categoryId === taxCategory.id,
71
+ )
72
+ ) {
73
+ await taxRateService.create(ctx, {
74
+ zoneId: taxZone.id,
75
+ categoryId: taxCategory.id,
76
+ name: "JarShop demo zero tax",
77
+ value: 0,
78
+ enabled: true,
79
+ });
80
+ }
81
+ const product =
82
+ existing ??
83
+ (await productService.create(ctx, {
84
+ enabled: true,
85
+ translations: [
86
+ {
87
+ languageCode: LanguageCode.sk,
88
+ name: "JarShop ukážkový produkt",
89
+ slug: productSlug,
90
+ description: "Generický produkt pre overenie JarShop startera.",
91
+ },
92
+ {
93
+ languageCode: LanguageCode.en,
94
+ name: "JarShop demo product",
95
+ slug: productSlug,
96
+ description:
97
+ "A generic product used to verify the JarShop starter.",
98
+ },
99
+ ],
100
+ }));
101
+
102
+ const existingVariants = existing
103
+ ? await variantService.getVariantsByProductId(ctx, existing.id, {
104
+ filter: { sku: { eq: "JARSHOP-DEMO-001" } },
105
+ })
106
+ : undefined;
107
+ if (existingVariants?.totalItems) {
108
+ console.log(`Seed skipped: ${productSlug} already exists`);
109
+ return;
110
+ }
111
+
112
+ await variantService.create(ctx, [
113
+ {
114
+ productId: product.id,
115
+ sku: "JARSHOP-DEMO-001",
116
+ enabled: true,
117
+ trackInventory: GlobalFlag.TRUE,
118
+ taxCategoryId: taxCategory.id,
119
+ stockLevels: [{ stockLocationId: stockLocation.id, stockOnHand: 10 }],
120
+ price: 1990,
121
+ translations: [
122
+ { languageCode: LanguageCode.sk, name: "JarShop ukážkový variant" },
123
+ { languageCode: LanguageCode.en, name: "JarShop demo variant" },
124
+ ],
125
+ },
126
+ ]);
127
+
128
+ console.log(`Seeded ${productSlug}`);
129
+ } finally {
130
+ await new Promise((resolve) => setTimeout(resolve, 1000));
131
+ await app.close();
132
+ }
133
+ }
134
+
135
+ seed().catch((error: unknown) => {
136
+ console.error(error);
137
+ process.exitCode = 1;
138
+ });
@@ -0,0 +1,75 @@
1
+ import {
2
+ DefaultJobQueuePlugin,
3
+ DefaultSchedulerPlugin,
4
+ DefaultSearchPlugin,
5
+ VendureConfig,
6
+ dummyPaymentHandler,
7
+ } from "@vendure/core";
8
+ import { AssetServerPlugin } from "@vendure/asset-server-plugin";
9
+ import { DashboardPlugin } from "@vendure/dashboard/plugin";
10
+ import {
11
+ EmailPlugin,
12
+ FileBasedTemplateLoader,
13
+ defaultEmailHandlers,
14
+ } from "@vendure/email-plugin";
15
+ import { GraphiqlPlugin } from "@vendure/graphiql-plugin";
16
+ import "dotenv/config";
17
+ import path from "node:path";
18
+
19
+ const isDev = process.env.APP_ENV === "dev";
20
+
21
+ export const config: VendureConfig = {
22
+ apiOptions: {
23
+ port: Number(process.env.PORT ?? 3000),
24
+ adminApiPath: "admin-api",
25
+ shopApiPath: "shop-api",
26
+ trustProxy: isDev ? false : 1,
27
+ ...(isDev ? { adminApiDebug: true, shopApiDebug: true } : {}),
28
+ },
29
+ authOptions: {
30
+ tokenMethod: ["bearer", "cookie"],
31
+ superadminCredentials: {
32
+ identifier: process.env.SUPERADMIN_USERNAME ?? "",
33
+ password: process.env.SUPERADMIN_PASSWORD ?? "",
34
+ },
35
+ cookieOptions: { secret: process.env.COOKIE_SECRET ?? "" },
36
+ },
37
+ dbConnectionOptions: {
38
+ type: "postgres",
39
+ synchronize: false,
40
+ migrations: [path.join(__dirname, "./migrations/*.+(js|ts)")],
41
+ logging: false,
42
+ database: process.env.DB_NAME,
43
+ schema: process.env.DB_SCHEMA,
44
+ host: process.env.DB_HOST,
45
+ port: Number(process.env.DB_PORT),
46
+ username: process.env.DB_USERNAME,
47
+ password: process.env.DB_PASSWORD,
48
+ },
49
+ paymentOptions: { paymentMethodHandlers: [dummyPaymentHandler] },
50
+ plugins: [
51
+ GraphiqlPlugin.init(),
52
+ AssetServerPlugin.init({
53
+ route: "assets",
54
+ assetUploadDir: path.join(__dirname, "../static/assets"),
55
+ }),
56
+ DefaultSchedulerPlugin.init(),
57
+ DefaultJobQueuePlugin.init({ useDatabaseForBuffer: true }),
58
+ DefaultSearchPlugin.init({ bufferUpdates: false, indexStockStatus: true }),
59
+ EmailPlugin.init({
60
+ devMode: true,
61
+ outputPath: path.join(__dirname, "../static/email/test-emails"),
62
+ route: "mailbox",
63
+ handlers: defaultEmailHandlers,
64
+ templateLoader: new FileBasedTemplateLoader(
65
+ path.join(__dirname, "../static/email/templates"),
66
+ ),
67
+ }),
68
+ DashboardPlugin.init({
69
+ route: "dashboard",
70
+ appDir: isDev
71
+ ? path.join(__dirname, "../dist/dashboard")
72
+ : path.join(__dirname, "dashboard"),
73
+ }),
74
+ ],
75
+ };
@@ -0,0 +1,20 @@
1
+ {{> header title="Verify Your New Email Address" }}
2
+
3
+ <mj-section background-color="#fafafa">
4
+ <mj-column>
5
+ <mj-text color="#525252">
6
+ We received a request to change your registered email address to this one.
7
+ Click the button below to verify this address and complete the process:
8
+ </mj-text>
9
+
10
+ <mj-button font-family="Helvetica"
11
+ background-color="#f45e43"
12
+ color="white"
13
+ href="{{ changeEmailAddressUrl }}?token={{ identifierChangeToken }}">
14
+ Verify Me!
15
+ </mj-button>
16
+ </mj-column>
17
+ </mj-section>
18
+
19
+
20
+ {{> footer }}
@@ -0,0 +1,20 @@
1
+ {{> header title="Verify Your Email Address" }}
2
+
3
+ <mj-section background-color="#fafafa">
4
+ <mj-column>
5
+ <mj-text color="#525252">
6
+ Thank you for creating an account. Click the button below to verify this email address and
7
+ complete the registration process:
8
+ </mj-text>
9
+
10
+ <mj-button font-family="Helvetica"
11
+ background-color="#f45e43"
12
+ color="white"
13
+ href="{{ verifyEmailAddressUrl }}?token={{ verificationToken }}">
14
+ Verify Me!
15
+ </mj-button>
16
+ </mj-column>
17
+ </mj-section>
18
+
19
+
20
+ {{> footer }}
@@ -0,0 +1,133 @@
1
+ {{> header title="Order Receipt" }}
2
+
3
+ <mj-raw>
4
+ <style type="text/css">
5
+ .callout {
6
+ background-color: #375a67;
7
+ padding: 15px 0;
8
+ }
9
+ .callout-large > div {
10
+ text-align: center !important;
11
+ color: #fff !important;
12
+ font-size: 16px !important;
13
+ font-weight: bold;
14
+ padding: 0;
15
+ }
16
+ .callout-small > div {
17
+ text-align: center !important;
18
+ color: #fff !important;
19
+ font-size: 14px !important;
20
+ padding: 0;
21
+ }
22
+ ul.address {
23
+ list-style-type: none;
24
+ padding: 0;
25
+ }
26
+ tr.order-row td {
27
+ border-bottom: 1px dashed #eee;
28
+ }
29
+ tr.order-row td:last-child {
30
+ text-align: center;
31
+ }
32
+ tr.total-row {
33
+ font-weight: bold;
34
+ }
35
+ .bg-off-white {
36
+ background-color: #f5f5f5;
37
+ }
38
+ </style>
39
+ </mj-raw>
40
+
41
+ <mj-section css-class="bg-off-white">
42
+ <mj-column>
43
+ <mj-text>
44
+ Dear {{ order.customer.firstName }} {{ order.customer.lastName }},
45
+ </mj-text>
46
+ <mj-text>
47
+ Thank you for your order!
48
+ </mj-text>
49
+ </mj-column>
50
+ </mj-section>
51
+
52
+
53
+ <mj-section css-class="callout">
54
+ <mj-column>
55
+ <mj-text css-class="callout-large"><strong>Order Code</strong></mj-text>
56
+ <mj-text css-class="callout-small">{{ order.code }}</mj-text>
57
+ </mj-column>
58
+ <mj-column>
59
+ <mj-text css-class="callout-large"><strong>Order Date</strong></mj-text>
60
+ <mj-text css-class="callout-small">{{ formatDate order.orderPlacedAt }}</mj-text>
61
+ </mj-column>
62
+ <mj-column>
63
+ <mj-text css-class="callout-large"><strong>Total Price</strong></mj-text>
64
+ <mj-text css-class="callout-small">{{ formatMoney order.total order.currencyCode 'en' }}</mj-text>
65
+ </mj-column>
66
+ </mj-section>
67
+
68
+
69
+ <mj-section css-class="bg-off-white">
70
+ <mj-column>
71
+ <mj-text>
72
+ {{#with order.shippingAddress }}
73
+ <h3>Shipping To: {{ fullName }}</h3>
74
+ <ul class="address">
75
+ {{#if company}}<li>{{ company }}</li>{{/if}}
76
+ {{#if streetLine1}}<li>{{ streetLine1 }}</li>{{/if}}
77
+ {{#if streetLine2}}<li>{{ streetLine2 }}</li>{{/if}}
78
+ {{#if city}}<li>{{ city }}</li>{{/if}}
79
+ {{#if province}}<li>{{ province }}</li>{{/if}}
80
+ {{#if postalCode}}<li>{{ postalCode }}</li>{{/if}}
81
+ {{#if country}}<li>{{ country }}</li>{{/if}}
82
+ {{#if phoneNumber}}<li>{{ phoneNumber }}</li>{{/if}}
83
+ </ul>
84
+ {{/with}}
85
+ </mj-text>
86
+ </mj-column>
87
+ </mj-section>
88
+
89
+ <mj-section>
90
+ <mj-column>
91
+ <mj-text>
92
+ <h3>Order Summary:</h3>
93
+ </mj-text>
94
+ <mj-table cellpadding="6px">
95
+ {{#each order.lines }}
96
+ <tr class="order-row">
97
+ <td>
98
+ <img alt="{{ productVariant.name }}"
99
+ style="width: 50px; height: 50px;"
100
+ src="{{ featuredAsset.preview }}?w=50&h=50" />
101
+ </td>
102
+ <td>{{ quantity }} x {{ productVariant.name }}</td>
103
+ <td>{{ productVariant.quantity }}</td>
104
+ <td>{{ formatMoney discountedLinePriceWithTax ../order.currencyCode 'en' }}</td>
105
+ </tr>
106
+ {{/each}}
107
+ {{#each order.discounts }}
108
+ <tr class="order-row">
109
+ <td colspan="3">
110
+ {{ description }}
111
+ </td>
112
+ <td>{{ formatMoney amount ../order.currencyCode 'en' }}</td>
113
+ </tr>
114
+ {{/each}}
115
+ <tr class="order-row">
116
+ <td colspan="3">Sub-total:</td>
117
+ <td>{{ formatMoney order.subTotalWithTax order.currencyCode 'en' }}</td>
118
+ </tr>
119
+ {{#each shippingLines }}
120
+ <tr class="order-row">
121
+ <td colspan="3">Shipping ({{ shippingMethod.name }}):</td>
122
+ <td>{{ formatMoney priceWithTax ../order.currencyCode 'en' }}</td>
123
+ </tr>
124
+ {{/each}}
125
+ <tr class="order-row total-row">
126
+ <td colspan="3">Total:</td>
127
+ <td>{{ formatMoney order.totalWithTax order.currencyCode 'en' }}</td>
128
+ </tr>
129
+ </mj-table>
130
+ </mj-column>
131
+ </mj-section>
132
+
133
+ {{> footer }}
@@ -0,0 +1,10 @@
1
+ <!--suppress ALL -->
2
+ <mj-section background-color="#375a67">
3
+ <mj-column width="100%">
4
+ <mj-text align="center" color="#eee">
5
+ <span>[footer text]</span>
6
+ </mj-text>
7
+ </mj-column>
8
+ </mj-section>
9
+ </mj-body>
10
+ </mjml>
@@ -0,0 +1,18 @@
1
+ <mjml>
2
+ <mj-head>
3
+ <mj-title>{{ title }}</mj-title>
4
+ <mj-style inline="inline">
5
+ h3 {
6
+ font-size: 18px;
7
+ color: #555;
8
+ font-weight: normal;
9
+ }
10
+ </mj-style>
11
+ </mj-head>
12
+
13
+ <mj-body>
14
+ <mj-section background-color="#f0f0f0">
15
+ <mj-column>
16
+ <mj-text>[company header]</mj-text>
17
+ </mj-column>
18
+ </mj-section>
@@ -0,0 +1,24 @@
1
+ {{> header title="Forgotten password reset" }}
2
+
3
+ <mj-section background-color="#fafafa">
4
+ <mj-column>
5
+ <mj-text color="#525252">
6
+ Someone requested a new password for your account.
7
+ </mj-text>
8
+
9
+ <mj-button font-family="Helvetica"
10
+ background-color="#f45e43"
11
+ color="white"
12
+ href="{{ passwordResetUrl }}?token={{ passwordResetToken }}">
13
+ Reset password
14
+ </mj-button>
15
+
16
+ <mj-text color="#525252">
17
+ If you didn't make this request then you can safely ignore this email - nothing has been changed on your account.
18
+ </mj-text>
19
+
20
+ </mj-column>
21
+ </mj-section>
22
+
23
+
24
+ {{> footer }}
@@ -0,0 +1,8 @@
1
+ # JarShop storefront tracer
2
+
3
+ The storefront is a neutral Next.js client project surface. It imports
4
+ commerce operations only from `lib/commerce.ts`, which is the private facade
5
+ over `@jarwizz/commerce-sdk`.
6
+
7
+ The `/sk` and `/en` routes intentionally render a safe empty state when the
8
+ Shop API is unavailable. A production build must not require a live API.
@@ -0,0 +1,52 @@
1
+ import { getProduct } from "../../../lib/commerce";
2
+
3
+ interface ProductPageProps {
4
+ params: Promise<{ locale: string; slug: string }>;
5
+ }
6
+
7
+ const labels = {
8
+ sk: {
9
+ back: "Späť",
10
+ notFound: "Produkt sa nenašiel.",
11
+ inStock: "Skladom",
12
+ outOfStock: "Vypredané",
13
+ },
14
+ en: {
15
+ back: "Back",
16
+ notFound: "Product not found.",
17
+ inStock: "In stock",
18
+ outOfStock: "Out of stock",
19
+ },
20
+ } as const;
21
+
22
+ export default async function ProductPage({ params }: ProductPageProps) {
23
+ const { locale, slug } = await params;
24
+ const language = locale === "en" ? "en" : "sk";
25
+ const result = await getProduct(slug, language);
26
+ const copy = labels[language];
27
+
28
+ if (!result.ok || !result.data) {
29
+ return (
30
+ <main lang={language}>
31
+ <p>{copy.notFound}</p>
32
+ </main>
33
+ );
34
+ }
35
+
36
+ return (
37
+ <main lang={language}>
38
+ <a href={`/${language}`}>← {copy.back}</a>
39
+ <h1>{result.data.name}</h1>
40
+ <p>{result.data.description}</p>
41
+ <ul>
42
+ {result.data.variants.map((variant) => (
43
+ <li key={variant.id}>
44
+ {variant.name}: {(variant.price.amount / 100).toFixed(2)}{" "}
45
+ {variant.price.currencyCode} —{" "}
46
+ {variant.inStock ? copy.inStock : copy.outOfStock}
47
+ </li>
48
+ ))}
49
+ </ul>
50
+ </main>
51
+ );
52
+ }
@@ -0,0 +1,35 @@
1
+ import { getCatalog } from "../../lib/commerce";
2
+
3
+ interface LocalePageProps {
4
+ params: Promise<{ locale: string }>;
5
+ }
6
+
7
+ const labels = {
8
+ sk: { title: "Produkty", empty: "Produkty sa nepodarilo načítať." },
9
+ en: { title: "Products", empty: "Products are not available." },
10
+ } as const;
11
+
12
+ export default async function LocalePage({ params }: LocalePageProps) {
13
+ const { locale } = await params;
14
+ const language = locale === "en" ? "en" : "sk";
15
+ const result = await getCatalog(language);
16
+ const copy = labels[language];
17
+
18
+ return (
19
+ <main lang={language}>
20
+ <h1>{copy.title}</h1>
21
+ {!result.ok || result.data.length === 0 ? (
22
+ <p>{copy.empty}</p>
23
+ ) : (
24
+ <ul>
25
+ {result.data.map((product) => (
26
+ <li key={product.id}>
27
+ <a href={`/${language}/${product.slug}`}>{product.name}</a>
28
+ <p>{product.description}</p>
29
+ </li>
30
+ ))}
31
+ </ul>
32
+ )}
33
+ </main>
34
+ );
35
+ }
@@ -0,0 +1,16 @@
1
+ :root {
2
+ color-scheme: light;
3
+ font-family: system-ui, sans-serif;
4
+ }
5
+
6
+ body {
7
+ margin: 0;
8
+ background: #fff;
9
+ color: #111;
10
+ }
11
+
12
+ main {
13
+ max-width: 72rem;
14
+ margin: 0 auto;
15
+ padding: 2rem;
16
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTypeScript from "eslint-config-next/typescript";
4
+
5
+ export default defineConfig([
6
+ ...nextVitals,
7
+ ...nextTypeScript,
8
+ globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
9
+ ]);
@@ -0,0 +1,35 @@
1
+ import { createCommerceClient } from "@jarwizz/commerce-sdk/server";
2
+ import type {
3
+ CommerceResult,
4
+ ProductSummary,
5
+ } from "@jarwizz/commerce-sdk/types";
6
+
7
+ export type CommerceLocale = "sk" | "en";
8
+
9
+ function endpointFor(locale: CommerceLocale): string {
10
+ const endpoint = new URL(
11
+ process.env.JARSHOP_COMMERCE_ENDPOINT ?? "http://localhost:3000/shop-api",
12
+ );
13
+ endpoint.searchParams.set("languageCode", locale);
14
+ return endpoint.toString();
15
+ }
16
+
17
+ function client(locale: CommerceLocale) {
18
+ return createCommerceClient({
19
+ endpoint: endpointFor(locale),
20
+ channelToken: process.env.JARSHOP_CHANNEL_TOKEN,
21
+ });
22
+ }
23
+
24
+ export function getCatalog(
25
+ locale: CommerceLocale,
26
+ ): Promise<CommerceResult<ProductSummary[]>> {
27
+ return client(locale).listProducts();
28
+ }
29
+
30
+ export function getProduct(
31
+ slug: string,
32
+ locale: CommerceLocale,
33
+ ): Promise<CommerceResult<ProductSummary | null>> {
34
+ return client(locale).getProductBySlug(slug);
35
+ }
@@ -0,0 +1,6 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+ import "./dist/types/routes.d.ts";
4
+
5
+ // NOTE: This file should not be edited
6
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -0,0 +1,14 @@
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ // Vendure's development watcher ignores dist directories. Keeping Next's
5
+ // generated TypeScript there prevents storefront HMR from restarting the
6
+ // server and worker.
7
+ distDir: "dist",
8
+ output: "standalone",
9
+ turbopack: {
10
+ root: import.meta.dirname,
11
+ },
12
+ };
13
+
14
+ export default nextConfig;
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@jarshop/storefront",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=24.18.0 <25"
8
+ },
9
+ "packageManager": "pnpm@11.18.0",
10
+ "scripts": {
11
+ "dev": "next dev",
12
+ "build": "next build",
13
+ "start": "next start",
14
+ "lint": "eslint .",
15
+ "typecheck": "tsc --noEmit"
16
+ },
17
+ "dependencies": {
18
+ "@jarwizz/commerce-sdk": "0.1.0",
19
+ "next": "16.2.12",
20
+ "react": "19.2.8",
21
+ "react-dom": "19.2.8"
22
+ },
23
+ "devDependencies": {
24
+ "@types/node": "24.13.3",
25
+ "@types/react": "19.2.14",
26
+ "@types/react-dom": "19.2.3",
27
+ "eslint": "9.39.5",
28
+ "eslint-config-next": "16.2.12",
29
+ "typescript-eslint": "8.65.0",
30
+ "typescript": "6.0.3"
31
+ }
32
+ }