create-brainerce-store 1.79.0 → 1.81.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/dist/index.js +168 -10
- package/messages/en.json +13 -4
- package/messages/he.json +13 -4
- package/package.json +10 -1
- package/templates/nextjs/base/.eslintrc.json +2 -0
- package/templates/nextjs/base/AI-GUIDE.md +32 -2
- package/templates/nextjs/base/package.json.ejs +53 -52
- package/templates/nextjs/base/scripts/connect.mjs +149 -0
- package/templates/nextjs/base/src/app/checkout/page.tsx +18 -0
- package/templates/nextjs/base/src/components/account/profile-section.tsx +7 -1
- package/templates/nextjs/base/src/components/auth/register-form.tsx +18 -1
- package/templates/nextjs/base/src/components/tracking-bootstrap.tsx +1 -1
- package/templates/nextjs/base/src/core/hooks/use-product-page.ts +22 -0
- package/templates/nextjs/base/src/core/lib/add-to-cart-error.ts +49 -0
- package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +18 -0
- package/templates/nextjs/base/src/ui/cart/cart-item.tsx +49 -0
- package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +96 -2
- package/templates/nextjs/base/src/ui/layout/newsletter-signup.tsx +59 -12
- package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +17 -0
- package/templates/nextjs/base/src/ui/product/product-card.tsx +17 -0
- package/templates/nextjs/base/src/ui/product/product-client-section.tsx +13 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-bundle-offer.tsx +18 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +49 -0
- package/templates/nextjs/designs/atelier/ui/cart/cart-upgrade-banner.tsx +101 -5
- package/templates/nextjs/designs/atelier/ui/product/frequently-bought-together.tsx +17 -0
- package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +17 -0
- package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +13 -0
- package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +16 -0
- package/templates/nextjs/ui-canvas/cart/cart-item.tsx +45 -0
- package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +95 -2
- package/templates/nextjs/ui-canvas/layout/newsletter-signup.tsx +50 -8
- package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +15 -0
- package/templates/nextjs/ui-canvas/product/product-card.tsx +15 -0
- package/templates/nextjs/ui-canvas/product/product-client-section.tsx +13 -0
package/dist/index.js
CHANGED
|
@@ -31,8 +31,17 @@ var require_package = __commonJS({
|
|
|
31
31
|
"package.json"(exports2, module2) {
|
|
32
32
|
module2.exports = {
|
|
33
33
|
name: "create-brainerce-store",
|
|
34
|
-
version: "1.
|
|
34
|
+
version: "1.81.0",
|
|
35
35
|
description: "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
36
|
+
homepage: "https://brainerce.com",
|
|
37
|
+
repository: {
|
|
38
|
+
type: "git",
|
|
39
|
+
url: "https://github.com/brainerce/brainerce.git",
|
|
40
|
+
directory: "packages/create-brainerce-store"
|
|
41
|
+
},
|
|
42
|
+
bugs: {
|
|
43
|
+
url: "https://github.com/brainerce/brainerce/issues"
|
|
44
|
+
},
|
|
36
45
|
bin: {
|
|
37
46
|
"create-brainerce-store": "dist/index.js"
|
|
38
47
|
},
|
|
@@ -315,7 +324,13 @@ var BRAINERCE_RUNTIME_DEPS = Object.freeze({
|
|
|
315
324
|
// off an SDK-typed `Product`, so on an older SDK a scaffolded store fails
|
|
316
325
|
// type-check at creation. Verified against the published 2.4.0 tarball: zero
|
|
317
326
|
// files mention either field.
|
|
318
|
-
|
|
327
|
+
//
|
|
328
|
+
// 2.7.0 is the first SDK with `marketing.getBenefit()` and
|
|
329
|
+
// `PublicNewsletterBenefitOffer`, and the scaffolded newsletter signup reads
|
|
330
|
+
// both to show the merchant's welcome offer beside the field. On an older
|
|
331
|
+
// SDK a scaffolded store fails type-check at creation, exactly like the KIT
|
|
332
|
+
// floor above — so this floor is load-bearing, not cosmetic.
|
|
333
|
+
brainerce: "^2.7.0",
|
|
319
334
|
"isomorphic-dompurify": "^3.8.0"
|
|
320
335
|
});
|
|
321
336
|
|
|
@@ -899,6 +914,105 @@ var logger = {
|
|
|
899
914
|
}
|
|
900
915
|
};
|
|
901
916
|
|
|
917
|
+
// src/device-flow.ts
|
|
918
|
+
var import_node_child_process = require("child_process");
|
|
919
|
+
function isOpenableUrl(value, apiBaseUrl) {
|
|
920
|
+
let url;
|
|
921
|
+
try {
|
|
922
|
+
url = new URL(value);
|
|
923
|
+
} catch {
|
|
924
|
+
return false;
|
|
925
|
+
}
|
|
926
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") return false;
|
|
927
|
+
const apiHost = (() => {
|
|
928
|
+
try {
|
|
929
|
+
return new URL(apiBaseUrl).hostname;
|
|
930
|
+
} catch {
|
|
931
|
+
return "";
|
|
932
|
+
}
|
|
933
|
+
})();
|
|
934
|
+
const base = apiHost.replace(/^api(-staging)?\./, "");
|
|
935
|
+
return url.hostname === base || url.hostname.endsWith(`.${base}`);
|
|
936
|
+
}
|
|
937
|
+
function openBrowser(url) {
|
|
938
|
+
const platform = process.platform;
|
|
939
|
+
const [cmd, args] = platform === "win32" ? ["cmd", ["/c", "start", "", url]] : platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
940
|
+
try {
|
|
941
|
+
const child = (0, import_node_child_process.spawn)(cmd, args, { stdio: "ignore", detached: true });
|
|
942
|
+
child.on("error", () => {
|
|
943
|
+
});
|
|
944
|
+
child.unref();
|
|
945
|
+
return true;
|
|
946
|
+
} catch {
|
|
947
|
+
return false;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
async function runDeviceFlow(apiBaseUrl, options = {}) {
|
|
951
|
+
let start;
|
|
952
|
+
try {
|
|
953
|
+
const res = await fetch(`${apiBaseUrl}/api/device-auth/start`, {
|
|
954
|
+
method: "POST",
|
|
955
|
+
headers: { "Content-Type": "application/json" },
|
|
956
|
+
body: JSON.stringify({
|
|
957
|
+
clientName: options.clientName || process.env.BRAINERCE_CLIENT_NAME || "create-brainerce-store"
|
|
958
|
+
})
|
|
959
|
+
});
|
|
960
|
+
if (!res.ok) {
|
|
961
|
+
logger.error(
|
|
962
|
+
`Could not start the browser approval (HTTP ${res.status}).
|
|
963
|
+
Create a sales channel in the dashboard instead, then re-run with --connection-id vc_xxx.`
|
|
964
|
+
);
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
start = await res.json();
|
|
968
|
+
} catch (error) {
|
|
969
|
+
logger.error(
|
|
970
|
+
`Could not reach Brainerce to start the approval: ${error instanceof Error ? error.message : String(error)}`
|
|
971
|
+
);
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
const opened = isOpenableUrl(start.verificationUriComplete, apiBaseUrl) && openBrowser(start.verificationUriComplete);
|
|
975
|
+
console.log();
|
|
976
|
+
logger.info(
|
|
977
|
+
opened ? " Opened your browser to approve this connection." : " Open this link to approve this connection:"
|
|
978
|
+
);
|
|
979
|
+
console.log(` ${start.verificationUriComplete}`);
|
|
980
|
+
console.log(` The page should show the code ${start.userCode} \u2014 check that it matches.`);
|
|
981
|
+
console.log();
|
|
982
|
+
logger.info(" Waiting for approval. A store and a sales channel are created if you have none.");
|
|
983
|
+
const deadline = Date.now() + start.expiresIn * 1e3;
|
|
984
|
+
let interval = (start.interval || 5) * 1e3;
|
|
985
|
+
while (Date.now() < deadline) {
|
|
986
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
987
|
+
let result;
|
|
988
|
+
try {
|
|
989
|
+
const res = await fetch(`${apiBaseUrl}/api/device-auth/poll`, {
|
|
990
|
+
method: "POST",
|
|
991
|
+
headers: { "Content-Type": "application/json" },
|
|
992
|
+
body: JSON.stringify({ deviceCode: start.deviceCode })
|
|
993
|
+
});
|
|
994
|
+
result = await res.json();
|
|
995
|
+
} catch {
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (result.status === "approved" && result.connectionId) {
|
|
999
|
+
logger.success(" Approved.");
|
|
1000
|
+
return { connectionId: result.connectionId, storeId: result.storeId ?? null };
|
|
1001
|
+
}
|
|
1002
|
+
if (result.status === "denied") {
|
|
1003
|
+
logger.error(" The connection request was declined. Nothing was created.");
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
if (result.status === "expired") {
|
|
1007
|
+
logger.error(" The code expired before it was approved. Run the command again.");
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
if (result.status === "slow_down") interval += 1e3;
|
|
1011
|
+
}
|
|
1012
|
+
logger.error(" Timed out waiting for approval. Run the command again for a fresh code.");
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
902
1016
|
// src/utils/spinner.ts
|
|
903
1017
|
var import_ora = __toESM(require("ora"));
|
|
904
1018
|
function createSpinner(text) {
|
|
@@ -944,6 +1058,7 @@ async function checkForUpdate(name, current) {
|
|
|
944
1058
|
return null;
|
|
945
1059
|
}
|
|
946
1060
|
}
|
|
1061
|
+
var DEFERRED_CONNECTION_ID = "vc_REPLACE_ME_RUN_npm_run_connect";
|
|
947
1062
|
async function resolveStoreInfoOrExit(connectionId, candidateApiUrls) {
|
|
948
1063
|
const spinner = createSpinner("Fetching store info...");
|
|
949
1064
|
spinner.start();
|
|
@@ -974,6 +1089,9 @@ async function resolveStoreInfoOrExit(connectionId, candidateApiUrls) {
|
|
|
974
1089
|
}
|
|
975
1090
|
var program = new import_commander.Command();
|
|
976
1091
|
program.name("create-brainerce-store").description("Scaffold a production-ready e-commerce storefront connected to Brainerce").version(pkg.version).argument("[project-name]", "Name for the project directory").option("--connection-id <id>", "Brainerce vibe-coded connection ID (vc_*)").option(
|
|
1092
|
+
"--defer-connection",
|
|
1093
|
+
"Scaffold now and connect later. Skips the browser approval and writes a placeholder id; finish with `npm run connect` inside the project."
|
|
1094
|
+
).option(
|
|
977
1095
|
"--api-url <url>",
|
|
978
1096
|
"Brainerce API base URL (overrides BRAINERCE_API_URL env, defaults to https://api.brainerce.com)"
|
|
979
1097
|
).option("--language <lang>", "Store language (en, he)").addOption(new import_commander.Option("--framework <framework>", "Framework to use").default("nextjs").hideHelp()).addOption(new import_commander.Option("--design <design>", "Storefront design pack").hideHelp()).option("--pkg-manager <manager>", "Package manager (npm, pnpm, yarn, bun)").option(
|
|
@@ -1043,11 +1161,28 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
|
|
|
1043
1161
|
let storeInfo = null;
|
|
1044
1162
|
let resolvedApiUrl = candidateApiUrls[0];
|
|
1045
1163
|
if (connectionId) {
|
|
1046
|
-
const
|
|
1047
|
-
if (
|
|
1048
|
-
logger.error(
|
|
1164
|
+
const connError = validateConnectionId(connectionId);
|
|
1165
|
+
if (connError) {
|
|
1166
|
+
logger.error(connError);
|
|
1167
|
+
process.exit(1);
|
|
1168
|
+
}
|
|
1169
|
+
const resolved = await resolveStoreInfoOrExit(connectionId, candidateApiUrls);
|
|
1170
|
+
storeInfo = resolved.info;
|
|
1171
|
+
resolvedApiUrl = resolved.apiBaseUrl;
|
|
1172
|
+
} else if (options.deferConnection === true) {
|
|
1173
|
+
connectionId = DEFERRED_CONNECTION_ID;
|
|
1174
|
+
storeInfo = {
|
|
1175
|
+
name: projectName || "My Store",
|
|
1176
|
+
storeName: projectName || "My Store",
|
|
1177
|
+
currency: "USD",
|
|
1178
|
+
language: language || "en"
|
|
1179
|
+
};
|
|
1180
|
+
} else {
|
|
1181
|
+
const approved = await runDeviceFlow(candidateApiUrls[0]);
|
|
1182
|
+
if (!approved) {
|
|
1049
1183
|
process.exit(1);
|
|
1050
1184
|
}
|
|
1185
|
+
connectionId = approved.connectionId;
|
|
1051
1186
|
const resolved = await resolveStoreInfoOrExit(connectionId, candidateApiUrls);
|
|
1052
1187
|
storeInfo = resolved.info;
|
|
1053
1188
|
resolvedApiUrl = resolved.apiBaseUrl;
|
|
@@ -1057,6 +1192,16 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
|
|
|
1057
1192
|
const storeSlug = storeInfo ? slugify(storeInfo.storeName) : "";
|
|
1058
1193
|
const projectNameSuggestion = channelSlug || storeSlug || void 0;
|
|
1059
1194
|
language = language || storeInfo?.language;
|
|
1195
|
+
if (!projectName && !process.stdin.isTTY) {
|
|
1196
|
+
if (!projectNameSuggestion) {
|
|
1197
|
+
logger.error(
|
|
1198
|
+
"No project name given, and no terminal to ask on. Pass one: npm create brainerce-store@latest my-store"
|
|
1199
|
+
);
|
|
1200
|
+
process.exit(1);
|
|
1201
|
+
}
|
|
1202
|
+
projectName = projectNameSuggestion;
|
|
1203
|
+
logger.info(` No project name given. Using "${projectName}", from your store.`);
|
|
1204
|
+
}
|
|
1060
1205
|
if (!projectName || !connectionId || !language) {
|
|
1061
1206
|
const answers = await runInteractive({
|
|
1062
1207
|
projectName,
|
|
@@ -1080,12 +1225,14 @@ program.name("create-brainerce-store").description("Scaffold a production-ready
|
|
|
1080
1225
|
logger.error(nameError);
|
|
1081
1226
|
process.exit(1);
|
|
1082
1227
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1228
|
+
if (connectionId !== DEFERRED_CONNECTION_ID) {
|
|
1229
|
+
const connError = validateConnectionId(connectionId);
|
|
1230
|
+
if (connError) {
|
|
1231
|
+
logger.error(connError);
|
|
1232
|
+
process.exit(1);
|
|
1233
|
+
}
|
|
1087
1234
|
}
|
|
1088
|
-
if (!storeInfo) {
|
|
1235
|
+
if (!storeInfo && connectionId !== DEFERRED_CONNECTION_ID) {
|
|
1089
1236
|
const resolved = await resolveStoreInfoOrExit(connectionId, candidateApiUrls);
|
|
1090
1237
|
storeInfo = resolved.info;
|
|
1091
1238
|
resolvedApiUrl = resolved.apiBaseUrl;
|
|
@@ -1181,7 +1328,18 @@ Your store will be running at http://localhost:3000
|
|
|
1181
1328
|
if (!scaffoldInPlace) {
|
|
1182
1329
|
logger.step(`cd ${projectName}`);
|
|
1183
1330
|
}
|
|
1331
|
+
if (connectionId === DEFERRED_CONNECTION_ID) {
|
|
1332
|
+
logger.step(`${pkgManager}${pkgManager === "npm" ? " run" : ""} connect`);
|
|
1333
|
+
}
|
|
1184
1334
|
logger.step(`${pkgManager}${pkgManager === "npm" ? " run" : ""} dev`);
|
|
1335
|
+
if (connectionId === DEFERRED_CONNECTION_ID) {
|
|
1336
|
+
logger.info(
|
|
1337
|
+
`
|
|
1338
|
+
This store is not connected yet. \`connect\` opens one browser approval,
|
|
1339
|
+
writes the sales channel id into .env.local, and creates a store and a
|
|
1340
|
+
channel for you if you have neither. Until then the catalog is empty.`
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1185
1343
|
logger.info(`
|
|
1186
1344
|
Your store will be running at http://localhost:3000`);
|
|
1187
1345
|
logger.info(
|
package/messages/en.json
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"birthday": "Birthday",
|
|
43
43
|
"birthdayMonth": "Month",
|
|
44
44
|
"birthdayDay": "Day",
|
|
45
|
-
"birthdayGiftNote": "Add your birthday and a gift arrives by email on the day.",
|
|
45
|
+
"birthdayGiftNote": "Add your birthday and a gift arrives by email on the day, as long as you are signed up for our emails.",
|
|
46
46
|
"birthdayIncomplete": "Choose both a month and a day, or leave both empty.",
|
|
47
47
|
"birthdayRequired": "Choose your birthday month and day to continue.",
|
|
48
48
|
"birthdayPlaceholder": "Choose your birthday",
|
|
@@ -104,6 +104,8 @@
|
|
|
104
104
|
"addToCart": "Add to Cart",
|
|
105
105
|
"addingToCart": "Adding...",
|
|
106
106
|
"addedToCart": "Added to Cart!",
|
|
107
|
+
"cartFull": "Your cart is full. Remove an item before adding this one.",
|
|
108
|
+
"addToCartFailed": "We could not add this to your cart. Please try again.",
|
|
107
109
|
"outOfStock": "Out of Stock",
|
|
108
110
|
"inStock": "In Stock",
|
|
109
111
|
"unavailable": "Unavailable",
|
|
@@ -122,7 +124,7 @@
|
|
|
122
124
|
"addSelectedToCart": "Add Selected to Cart",
|
|
123
125
|
"totalPrice": "Total: {price}",
|
|
124
126
|
"addingAll": "Adding...",
|
|
125
|
-
|
|
127
|
+
"whatsInTheBox": "What's in the box",
|
|
126
128
|
"by": "By"
|
|
127
129
|
},
|
|
128
130
|
"reviews": {
|
|
@@ -191,12 +193,17 @@
|
|
|
191
193
|
"upgrade": "Upgrade",
|
|
192
194
|
"upgrading": "Upgrading...",
|
|
193
195
|
"dismissUpgrade": "Dismiss",
|
|
196
|
+
"upgradeOriginalNotRemoved": "We added the upgrade to your cart, but we could not remove the original item. Press Upgrade again to remove it.",
|
|
194
197
|
"bundleOffers": "Bundle & Save",
|
|
195
198
|
"addBundleItem": "Add & Save",
|
|
196
199
|
"addingBundle": "Adding...",
|
|
197
200
|
"selectOptions": "Select options",
|
|
198
201
|
"outOfStock": "Out of stock",
|
|
199
|
-
"unavailableItemsHint": "Remove the items marked out of stock or unavailable to continue to checkout."
|
|
202
|
+
"unavailableItemsHint": "Remove the items marked out of stock or unavailable to continue to checkout.",
|
|
203
|
+
"notEnoughStock": "There is not enough stock left for that quantity.",
|
|
204
|
+
"itemUnavailable": "This item can no longer be bought. Remove it to continue.",
|
|
205
|
+
"quantityUpdateFailed": "We could not update the quantity. Please try again.",
|
|
206
|
+
"removeFailed": "We could not remove this item. Please try again."
|
|
200
207
|
},
|
|
201
208
|
"checkout": {
|
|
202
209
|
"pageTitle": "Checkout",
|
|
@@ -569,7 +576,9 @@
|
|
|
569
576
|
"submitting": "Sending...",
|
|
570
577
|
"checkEmailTitle": "Almost there — check your email",
|
|
571
578
|
"checkEmailBody": "We've sent you a link to confirm. Look in your spam folder too; you won't get another copy for 24 hours.",
|
|
572
|
-
"
|
|
579
|
+
"offerPercent": "{value}% off your first order",
|
|
580
|
+
"offerAmount": "{value} off your first order",
|
|
581
|
+
"offerByEmail": "Your personal code is in that email too. It is one per address.",
|
|
573
582
|
"genericError": "Something went wrong. Please try again."
|
|
574
583
|
},
|
|
575
584
|
"loyalty": {
|
package/messages/he.json
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"birthday": "יום הולדת",
|
|
43
43
|
"birthdayMonth": "חודש",
|
|
44
44
|
"birthdayDay": "יום",
|
|
45
|
-
"birthdayGiftNote": "הוסיפו את יום ההולדת ותקבלו מתנה במייל ביום
|
|
45
|
+
"birthdayGiftNote": "הוסיפו את יום ההולדת ותקבלו מתנה במייל ביום עצמו, בתנאי שאתם רשומים לדיוור שלנו.",
|
|
46
46
|
"birthdayIncomplete": "בחרו גם חודש וגם יום, או השאירו את שניהם ריקים.",
|
|
47
47
|
"birthdayRequired": "בחרו את חודש ויום ההולדת כדי להמשיך.",
|
|
48
48
|
"birthdayPlaceholder": "בחרו את יום ההולדת",
|
|
@@ -104,6 +104,8 @@
|
|
|
104
104
|
"addToCart": "הוסף לעגלה",
|
|
105
105
|
"addingToCart": "מוסיף...",
|
|
106
106
|
"addedToCart": "נוסף לעגלה!",
|
|
107
|
+
"cartFull": "העגלה מלאה. הסירו מוצר אחד לפני הוספת המוצר הזה.",
|
|
108
|
+
"addToCartFailed": "לא הצלחנו להוסיף את המוצר לעגלה. נסו שוב.",
|
|
107
109
|
"outOfStock": "אזל מהמלאי",
|
|
108
110
|
"inStock": "במלאי",
|
|
109
111
|
"unavailable": "לא זמין",
|
|
@@ -122,7 +124,7 @@
|
|
|
122
124
|
"addSelectedToCart": "הוסף הנבחרים לעגלה",
|
|
123
125
|
"totalPrice": "סה\"כ: {price}",
|
|
124
126
|
"addingAll": "מוסיף...",
|
|
125
|
-
|
|
127
|
+
"whatsInTheBox": "מה יש בקופסה",
|
|
126
128
|
"by": "מאת"
|
|
127
129
|
},
|
|
128
130
|
"reviews": {
|
|
@@ -191,12 +193,17 @@
|
|
|
191
193
|
"upgrade": "שדרג",
|
|
192
194
|
"upgrading": "משדרג...",
|
|
193
195
|
"dismissUpgrade": "סגור",
|
|
196
|
+
"upgradeOriginalNotRemoved": "הוספנו את השדרוג לעגלה, אבל לא הצלחנו להסיר את המוצר המקורי. לחצו שוב על שדרוג כדי להסיר אותו.",
|
|
194
197
|
"bundleOffers": "חבילה וחיסכון",
|
|
195
198
|
"addBundleItem": "הוסף וחסוך",
|
|
196
199
|
"addingBundle": "מוסיף...",
|
|
197
200
|
"selectOptions": "בחר אפשרויות",
|
|
198
201
|
"outOfStock": "אזל מהמלאי",
|
|
199
|
-
"unavailableItemsHint": "הסירו את המוצרים שמסומנים כאזלו מהמלאי או כלא זמינים כדי להמשיך לתשלום."
|
|
202
|
+
"unavailableItemsHint": "הסירו את המוצרים שמסומנים כאזלו מהמלאי או כלא זמינים כדי להמשיך לתשלום.",
|
|
203
|
+
"notEnoughStock": "אין מספיק מלאי לכמות הזו.",
|
|
204
|
+
"itemUnavailable": "לא ניתן לרכוש את המוצר הזה יותר. הסירו אותו כדי להמשיך.",
|
|
205
|
+
"quantityUpdateFailed": "לא הצלחנו לעדכן את הכמות. נסו שוב.",
|
|
206
|
+
"removeFailed": "לא הצלחנו להסיר את המוצר. נסו שוב."
|
|
200
207
|
},
|
|
201
208
|
"checkout": {
|
|
202
209
|
"pageTitle": "תשלום",
|
|
@@ -569,7 +576,9 @@
|
|
|
569
576
|
"submitting": "שולח...",
|
|
570
577
|
"checkEmailTitle": "כמעט סיימנו — בדקו את המייל",
|
|
571
578
|
"checkEmailBody": "שלחנו לכם קישור לאישור. בדקו גם בתיקיית הספאם; עותק נוסף לא יישלח ב-24 השעות הקרובות.",
|
|
572
|
-
"
|
|
579
|
+
"offerPercent": "{value}% הנחה על ההזמנה הראשונה",
|
|
580
|
+
"offerAmount": "{value} הנחה על ההזמנה הראשונה",
|
|
581
|
+
"offerByEmail": "הקוד האישי שלכם נמצא באותו מייל. הטבה אחת לכל כתובת.",
|
|
573
582
|
"genericError": "משהו השתבש. נסו שוב."
|
|
574
583
|
},
|
|
575
584
|
"loyalty": {
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-brainerce-store",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.81.0",
|
|
4
4
|
"description": "Scaffold a production-ready e-commerce storefront connected to Brainerce",
|
|
5
|
+
"homepage": "https://brainerce.com",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/brainerce/brainerce.git",
|
|
9
|
+
"directory": "packages/create-brainerce-store"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/brainerce/brainerce/issues"
|
|
13
|
+
},
|
|
5
14
|
"bin": {
|
|
6
15
|
"create-brainerce-store": "dist/index.js"
|
|
7
16
|
},
|
|
@@ -59,8 +59,38 @@ UI components get *everything* from `@/core/hooks/*` and
|
|
|
59
59
|
- `useHomeData()` → `{ products, banners, loading }`
|
|
60
60
|
- `useProductListing()` → products + filters/sort/facets/pagination + handlers
|
|
61
61
|
- `useProductPage(product)` → variant/image selection, `priceInfo`, `inventory`,
|
|
62
|
-
`quantity`, `handleAddToCart`, `addingToCart`, `addedMessage`,
|
|
63
|
-
+ modifier state and errors
|
|
62
|
+
`quantity`, `handleAddToCart`, `addingToCart`, `addedMessage`, `addToCartError`,
|
|
63
|
+
customization + modifier state and errors
|
|
64
|
+
- `addToCartError` is `'CART_FULL' | 'FAILED' | null`. **Render it, and render it
|
|
65
|
+
outside any `modifierGroups.length > 0` block** — a cart holds at most 50
|
|
66
|
+
distinct products and the 51st add is refused, so a design that drops this
|
|
67
|
+
leaves the shopper tapping a button that resets and adds nothing. Copy lives
|
|
68
|
+
in `productDetail.cartFull` / `productDetail.addToCartFailed`.
|
|
69
|
+
- Five more components hold the same message in their OWN local state, because
|
|
70
|
+
they add to the cart without this hook: `ui/product/product-card.tsx`,
|
|
71
|
+
`ui/product/frequently-bought-together.tsx`, `ui/cart/cart-bundle-offer.tsx`
|
|
72
|
+
(all three: `addError`), `app/checkout/page.tsx` (`bumpError`) and
|
|
73
|
+
`ui/cart/cart-upgrade-banner.tsx` (`upgradeError`). All five use
|
|
74
|
+
`toAddToCartError()` from `@/core/lib/add-to-cart-error`. **Nothing checks that
|
|
75
|
+
you kept them** — `check-template-parity.js` compares `@/ui/...` imports and SDK
|
|
76
|
+
calls only, so dropping one of these leaves a green build and a silent button.
|
|
77
|
+
- `ui/cart/cart-item.tsx` carries the same idea for the two writes it owns,
|
|
78
|
+
in its own `lineError` state: a failed quantity change or a failed Remove
|
|
79
|
+
used to log to the console and leave the row looking untouched. It uses
|
|
80
|
+
`getErrorCode()` from the same file (not `toAddToCartError()` — the 50-line
|
|
81
|
+
cap cannot refuse a quantity change, and "we could not add this" would be a
|
|
82
|
+
lie about a removal) and four `cart.*` strings, mapped in `LINE_ERROR_KEYS`:
|
|
83
|
+
`notEnoughStock`, `itemUnavailable`, `quantityUpdateFailed`, `removeFailed`.
|
|
84
|
+
The first two are for refusals a retry can never clear, so do not collapse
|
|
85
|
+
them into the generic one. Nothing checks that you kept any of this either.
|
|
86
|
+
- `ui/cart/cart-upgrade-banner.tsx` additionally swaps one cart line for
|
|
87
|
+
another, and the ORDER of its two SDK calls is a correctness property, not a
|
|
88
|
+
style choice: it **adds the upgrade first and removes the original second**,
|
|
89
|
+
so a failed add leaves the cart untouched instead of losing the shopper's
|
|
90
|
+
line. It also carries `upgradeAdded`, which stops a retry adding the upgrade
|
|
91
|
+
twice, and a third message, `cart.upgradeOriginalNotRemoved`, for the case
|
|
92
|
+
where the add landed and the removal did not. If you rebuild that component,
|
|
93
|
+
keep all three; the reasoning is in the JSDoc on `handleUpgrade`.
|
|
64
94
|
- `useCartPage()` / `useCart()` → `{ cart, itemCount, totals, refreshCart }`
|
|
65
95
|
|
|
66
96
|
Hooks return data and handlers, never JSX. All catalog content (names, prices,
|
|
@@ -1,52 +1,53 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "<%= projectName %>",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"scripts": {
|
|
6
|
-
"dev": "next dev",
|
|
7
|
-
"build": "next build",
|
|
8
|
-
"start": "next start",
|
|
9
|
-
"lint": "next lint",
|
|
10
|
-
"setup": "node scripts/fetch-store-info.mjs"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"react
|
|
17
|
-
"
|
|
18
|
-
"@radix-ui/react-
|
|
19
|
-
"@radix-ui/react-
|
|
20
|
-
"@radix-ui/react-
|
|
21
|
-
"@radix-ui/react-
|
|
22
|
-
"@radix-ui/react-
|
|
23
|
-
"@radix-ui/react-
|
|
24
|
-
"@radix-ui/react-
|
|
25
|
-
"@radix-ui/react-
|
|
26
|
-
"@radix-ui/react-
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"@types/
|
|
38
|
-
"@types/react
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"eslint
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "<%= projectName %>",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start",
|
|
9
|
+
"lint": "next lint",
|
|
10
|
+
"setup": "node scripts/fetch-store-info.mjs",
|
|
11
|
+
"connect": "node scripts/connect.mjs"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"brainerce": "<%= brainerceVersion %>",
|
|
15
|
+
"next": "^15.3.4",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0",
|
|
18
|
+
"@radix-ui/react-accordion": "^1.2.0",
|
|
19
|
+
"@radix-ui/react-checkbox": "^1.1.1",
|
|
20
|
+
"@radix-ui/react-dialog": "^1.1.2",
|
|
21
|
+
"@radix-ui/react-label": "^2.1.0",
|
|
22
|
+
"@radix-ui/react-radio-group": "^1.2.0",
|
|
23
|
+
"@radix-ui/react-select": "^2.1.1",
|
|
24
|
+
"@radix-ui/react-separator": "^1.1.0",
|
|
25
|
+
"@radix-ui/react-slot": "^1.1.0",
|
|
26
|
+
"@radix-ui/react-tabs": "^1.1.0",
|
|
27
|
+
"@radix-ui/react-tooltip": "^1.1.4",
|
|
28
|
+
"class-variance-authority": "^0.7.1",
|
|
29
|
+
"clsx": "^2.1.1",
|
|
30
|
+
"lucide-react": "^0.462.0",
|
|
31
|
+
"tailwind-merge": "^2.5.2",
|
|
32
|
+
"tailwindcss-animate": "^1.0.7",
|
|
33
|
+
"isomorphic-dompurify": "<%= isomorphicDompurifyVersion %>",
|
|
34
|
+
"sharp": "^0.35.3"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/node": "^20.0.0",
|
|
38
|
+
"@types/react": "^19.0.0",
|
|
39
|
+
"@types/react-dom": "^19.0.0",
|
|
40
|
+
"autoprefixer": "^10.4.0",
|
|
41
|
+
"eslint": "^9.0.0",
|
|
42
|
+
"eslint-config-next": "^15.3.4",
|
|
43
|
+
"postcss": "^8.4.49",
|
|
44
|
+
"tailwindcss": "^3.4.0",
|
|
45
|
+
"typescript": "^5.4.0"
|
|
46
|
+
},
|
|
47
|
+
"engines": {
|
|
48
|
+
"node": "^20.19.0 || ^22.13.0 || >=24.0.0"
|
|
49
|
+
},
|
|
50
|
+
"pnpm": {
|
|
51
|
+
"onlyBuiltDependencies": ["sharp"]
|
|
52
|
+
}
|
|
53
|
+
}
|