@osimart/create-storefront 0.0.0-stage → 1.0.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.
- package/README.md +80 -2
- package/THEME_DEV_API.md +80 -0
- package/app/app.vue +154 -0
- package/app/assets/css/osi-controls.css +824 -0
- package/app/components/SectionRenderer.vue +46 -0
- package/app/components/builder/BuilderOverlay.vue +582 -0
- package/app/components/checkout/ExtraFees.vue +329 -0
- package/app/components/checkout/PaymentHandoff.vue +388 -0
- package/app/components/common/Banner.vue +35 -0
- package/app/components/common/ConfirmDeleteModal.vue +117 -0
- package/app/components/common/DropdownSelect.vue +432 -0
- package/app/components/common/Loader.vue +32 -0
- package/app/components/common/OsiField.vue +218 -0
- package/app/components/common/PhoneNumInput.vue +434 -0
- package/app/composables/UseBodyScrollLock.js +122 -0
- package/app/composables/UseThemeGroup.js +575 -0
- package/app/composables/useAddToCart.js +87 -0
- package/app/composables/useAnalytics.js +15 -0
- package/app/composables/useAnnouncements.js +29 -0
- package/app/composables/useApiErrorToast.js +104 -0
- package/app/composables/useAuth.js +99 -0
- package/app/composables/useBanners.js +25 -0
- package/app/composables/useBuilderPreview.js +320 -0
- package/app/composables/useButtonStyle.js +107 -0
- package/app/composables/useCarousel.js +160 -0
- package/app/composables/useCart.js +109 -0
- package/app/composables/useCheckout.js +76 -0
- package/app/composables/useClassificationItems.js +269 -0
- package/app/composables/useClassificationPage.js +545 -0
- package/app/composables/useClassificationUrl.js +63 -0
- package/app/composables/useColorScheme.js +44 -0
- package/app/composables/useContactForm.js +53 -0
- package/app/composables/useContentTree.js +770 -0
- package/app/composables/useDragScroll.js +73 -0
- package/app/composables/useFooterSocials.js +107 -0
- package/app/composables/useInputStyle.js +45 -0
- package/app/composables/useLayoutShell.js +114 -0
- package/app/composables/useLazyVideo.js +36 -0
- package/app/composables/useMediaTypes.js +66 -0
- package/app/composables/useMediaUrl.js +71 -0
- package/app/composables/useMenu.js +100 -0
- package/app/composables/useNavStyle.js +219 -0
- package/app/composables/usePlaceholders.js +294 -0
- package/app/composables/usePolicies.js +26 -0
- package/app/composables/useProductCardMedia.js +135 -0
- package/app/composables/useProductCardStyle.js +132 -0
- package/app/composables/useProductDetail.js +982 -0
- package/app/composables/useProductGallery.js +166 -0
- package/app/composables/useProductItems.js +270 -0
- package/app/composables/useProductUrl.js +60 -0
- package/app/composables/useProductVariants.js +210 -0
- package/app/composables/useQuickShop.js +115 -0
- package/app/composables/useRelatedProducts.js +186 -0
- package/app/composables/useRenderContext.js +45 -0
- package/app/composables/useScopedCss.js +106 -0
- package/app/composables/useSearch.js +35 -0
- package/app/composables/useSectionData.js +462 -0
- package/app/composables/useSectionShell.js +113 -0
- package/app/composables/useSidebar.js +69 -0
- package/app/composables/useStoreData.js +57 -0
- package/app/composables/useStoreInit.js +52 -0
- package/app/composables/useStorefront.js +239 -0
- package/app/composables/useT.js +21 -0
- package/app/composables/useTextStyles.js +84 -0
- package/app/composables/useThemeControls.js +34 -0
- package/app/composables/useThemeSeo.js +91 -0
- package/app/composables/useThemeTokens.js +171 -0
- package/app/composables/useWishlist.js +66 -0
- package/app/logic/components/AnnouncementBar.js +118 -0
- package/app/logic/components/CardGrid.js +125 -0
- package/app/logic/components/CardList.js +98 -0
- package/app/logic/components/CartDrawer.js +273 -0
- package/app/logic/components/CartItem.js +31 -0
- package/app/logic/components/ClassificationBento.js +6 -0
- package/app/logic/components/ClassificationCard.js +6 -0
- package/app/logic/components/ClassificationCarousel.js +115 -0
- package/app/logic/components/ClassificationEditorial.js +41 -0
- package/app/logic/components/ClassificationFeatured.js +57 -0
- package/app/logic/components/ClassificationGrid.js +6 -0
- package/app/logic/components/ClassificationNav.js +61 -0
- package/app/logic/components/ClassificationPart.js +72 -0
- package/app/logic/components/FooterBlock.js +91 -0
- package/app/logic/components/FooterSocials.js +60 -0
- package/app/logic/components/HeaderDropdownList.js +29 -0
- package/app/logic/components/HeaderIcon.js +68 -0
- package/app/logic/components/HeaderLogo.js +39 -0
- package/app/logic/components/HeaderNav.js +268 -0
- package/app/logic/components/MobileMenu.js +280 -0
- package/app/logic/components/ProductBuyButtons.js +66 -0
- package/app/logic/components/ProductCard.js +160 -0
- package/app/logic/components/ProductFilters.js +37 -0
- package/app/logic/components/ProductGallery.js +19 -0
- package/app/logic/components/ProductInfoBlock.js +411 -0
- package/app/logic/components/ProductLightbox.js +12 -0
- package/app/logic/components/ProductQuantity.js +31 -0
- package/app/logic/components/ProductVariantPicker.js +125 -0
- package/app/logic/components/QuickBuy.js +194 -0
- package/app/logic/components/SearchOverlay.js +41 -0
- package/app/logic/components/SidebarMenuItem.js +39 -0
- package/app/logic/components/SiteFooter.js +476 -0
- package/app/logic/components/SiteHeader.js +266 -0
- package/app/logic/components/WhatsAppButton.js +75 -0
- package/app/logic/layouts/default.js +66 -0
- package/app/logic/sections/AnnouncementBarSection.js +6 -0
- package/app/logic/sections/BlockCarousel.js +103 -0
- package/app/logic/sections/ClassificationListSection.js +40 -0
- package/app/logic/sections/ContactFormSection.js +294 -0
- package/app/logic/sections/ContactInfoSection.js +419 -0
- package/app/logic/sections/ContentNode.js +189 -0
- package/app/logic/sections/CountdownSection.js +254 -0
- package/app/logic/sections/CustomHtmlSection.js +51 -0
- package/app/logic/sections/DividerSection.js +31 -0
- package/app/logic/sections/FaqSection.js +53 -0
- package/app/logic/sections/FeatureListSection.js +6 -0
- package/app/logic/sections/FeaturedProductSection.js +40 -0
- package/app/logic/sections/FooterSection.js +16 -0
- package/app/logic/sections/FooterUtilitiesSection.js +24 -0
- package/app/logic/sections/GallerySection.js +232 -0
- package/app/logic/sections/HeaderSection.js +24 -0
- package/app/logic/sections/HeroSection.js +24 -0
- package/app/logic/sections/LogoListSection.js +6 -0
- package/app/logic/sections/MainClassificationSection.js +473 -0
- package/app/logic/sections/MainProductSection.js +211 -0
- package/app/logic/sections/MainSearchSection.js +6 -0
- package/app/logic/sections/MapSection.js +92 -0
- package/app/logic/sections/MediaContentSection.js +38 -0
- package/app/logic/sections/MediaWithTextSection.js +238 -0
- package/app/logic/sections/NewsletterSection.js +126 -0
- package/app/logic/sections/ProductHighlightSection.js +201 -0
- package/app/logic/sections/ProductListSection.js +45 -0
- package/app/logic/sections/ProductRecommendationsSection.js +113 -0
- package/app/logic/sections/RichTextSection.js +6 -0
- package/app/logic/sections/ScrollingTextSection.js +69 -0
- package/app/logic/sections/SearchHeaderSection.js +22 -0
- package/app/logic/sections/SlideshowSection.js +118 -0
- package/app/logic/sections/SpacerSection.js +37 -0
- package/app/logic/sections/SplitShowcaseSection.js +6 -0
- package/app/logic/sections/TestimonialListSection.js +6 -0
- package/app/logic/sections/VideoSection.js +235 -0
- package/app/pages/[category]/[product].vue +196 -0
- package/app/pages/__builder-preview.vue +38 -0
- package/app/pages/categories/[slug].vue +208 -0
- package/app/pages/checkout.vue +1972 -0
- package/app/pages/forgot-password.vue +115 -0
- package/app/pages/index.vue +64 -0
- package/app/pages/login.vue +585 -0
- package/app/pages/order-detail.vue +442 -0
- package/app/pages/pages/[handle].vue +58 -0
- package/app/pages/privacy-policy.vue +46 -0
- package/app/pages/resetpassword.vue +138 -0
- package/app/pages/resetverificationcode.vue +163 -0
- package/app/pages/result.vue +203 -0
- package/app/pages/return-policy.vue +50 -0
- package/app/pages/search.vue +100 -0
- package/app/pages/shipping-policy.vue +46 -0
- package/app/pages/user-profile/components/AddAddressModal.vue +452 -0
- package/app/pages/user-profile/components/Whishlist.vue +137 -0
- package/app/pages/user-profile/components/left-bar.vue +233 -0
- package/app/pages/user-profile/components/orders.vue +162 -0
- package/app/pages/user-profile/components/profile-details.vue +566 -0
- package/app/pages/user-profile/components/settings.vue +194 -0
- package/app/pages/user-profile/index.vue +252 -0
- package/app/pages/verificationcode.vue +188 -0
- package/app/plugins/analytics.client.js +99 -0
- package/app/plugins/geo.js +24 -0
- package/app/plugins/reveal.client.js +53 -0
- package/app/plugins/toast.js +10 -0
- package/app/plugins/vue-query.js +5 -0
- package/app/services/ApiServices.js +74 -0
- package/app/services/AuthBus.js +162 -0
- package/app/services/Authentication/AuthenticationService.js +133 -0
- package/app/services/Authentication/TokenService.js +62 -0
- package/app/services/CartServices.js +91 -0
- package/app/services/CartToken.js +26 -0
- package/app/services/CategoryServices.js +54 -0
- package/app/services/CheckoutServices.js +113 -0
- package/app/services/HomeServices.js +133 -0
- package/app/services/NewsletterServices.js +29 -0
- package/app/services/PaymentServices.js +71 -0
- package/app/services/PoliciesServices.js +16 -0
- package/app/services/ProductServices.js +79 -0
- package/app/services/SearchServices.js +18 -0
- package/app/services/UserServices.js +105 -0
- package/app/services/UtilsServices.js +46 -0
- package/app/services/WishlistServices.js +51 -0
- package/app/services/storefront.service.js +57 -0
- package/app/stores/userStore.js +13 -0
- package/app/theme/categoryTree.js +68 -0
- package/app/theme/controls.js +88 -0
- package/app/theme/registry.js +56 -0
- package/app/utils/analytics.js +864 -0
- package/app/utils/device.js +50 -0
- package/app/utils/eventBus.js +24 -0
- package/app/utils/icon-presets.js +24 -0
- package/app/utils/productOffers.js +118 -0
- package/app/utils/requestCache.js +36 -0
- package/app/utils/scrollToError.js +59 -0
- package/app/utils/toPlainError.js +30 -0
- package/bin/osimart-theme.mjs +282 -0
- package/nuxt/osi-sections.ts +56 -0
- package/nuxt.config.ts +60 -0
- package/package.json +44 -4
- package/public/css/input.css +29 -0
- package/public/css/output.css +2716 -0
- package/public/favicon-32x32.png +0 -0
- package/public/robots.txt +2 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { apiService, getBaseUrl } from "./ApiServices";
|
|
2
|
+
|
|
3
|
+
const getUserDetail = async (token) => {
|
|
4
|
+
try {
|
|
5
|
+
const baseUrl = getBaseUrl();
|
|
6
|
+
const responseData = await apiService(`${baseUrl}store/apis/customer-info/`, {
|
|
7
|
+
method: "GET",
|
|
8
|
+
});
|
|
9
|
+
return responseData;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error('Cart fetch error:', error);
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const getUserOrders = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const baseUrl = getBaseUrl();
|
|
19
|
+
const responseData = await apiService(`${baseUrl}store/apis/order-summaries/`, {
|
|
20
|
+
method: "GET",
|
|
21
|
+
});
|
|
22
|
+
return responseData;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error('Cart fetch error:', error);
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const updateUserDetail = async (data) => {
|
|
30
|
+
try {
|
|
31
|
+
const baseUrl = getBaseUrl();
|
|
32
|
+
const responseData = await apiService(`${baseUrl}store/apis/customer-info/modify/`, {
|
|
33
|
+
method: "PUT",
|
|
34
|
+
body: JSON.stringify(data),
|
|
35
|
+
});
|
|
36
|
+
return responseData;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.error('update user error:', error);
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const addAddress = async (address, country, postal_code) => {
|
|
44
|
+
try {
|
|
45
|
+
const baseUrl = getBaseUrl();
|
|
46
|
+
const responseData = await apiService(`${baseUrl}store/apis/customer-addresses/`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
body: JSON.stringify({ address, country, postal_code }),
|
|
49
|
+
});
|
|
50
|
+
return responseData;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Cart fetch error:', error);
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const editAddress = async (id, address, country, postal_code) => {
|
|
58
|
+
try {
|
|
59
|
+
const baseUrl = getBaseUrl();
|
|
60
|
+
const responseData = await apiService(`${baseUrl}store/apis/customer-addresses/${id}/`, {
|
|
61
|
+
method: "PATCH",
|
|
62
|
+
body: JSON.stringify({ address, country, postal_code }),
|
|
63
|
+
});
|
|
64
|
+
return responseData;
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.error('Cart fetch error:', error);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const deleteAddress = async (id) => {
|
|
72
|
+
try {
|
|
73
|
+
const baseUrl = getBaseUrl();
|
|
74
|
+
const responseData = await apiService(`${baseUrl}store/apis/customer-addresses/${id}/`, {
|
|
75
|
+
method: "DELETE",
|
|
76
|
+
});
|
|
77
|
+
return responseData;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error("Cart fetch error:", error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const contact = async (first_name, last_name, email, subject, message) => {
|
|
85
|
+
try {
|
|
86
|
+
const baseUrl = getBaseUrl();
|
|
87
|
+
const responseData = await apiService(`${baseUrl}store/apis/contactmessage/`, {
|
|
88
|
+
method: "POST",
|
|
89
|
+
body: JSON.stringify({ first_name, last_name, email, subject, message }),
|
|
90
|
+
});
|
|
91
|
+
return responseData;
|
|
92
|
+
} catch (error) {
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export default {
|
|
98
|
+
getUserDetail,
|
|
99
|
+
getUserOrders,
|
|
100
|
+
addAddress,
|
|
101
|
+
editAddress,
|
|
102
|
+
deleteAddress,
|
|
103
|
+
updateUserDetail,
|
|
104
|
+
contact,
|
|
105
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { apiService, getBaseUrl } from "./ApiServices";
|
|
2
|
+
|
|
3
|
+
const getCountries = async () => {
|
|
4
|
+
try {
|
|
5
|
+
const baseUrl = getBaseUrl();
|
|
6
|
+
const responseData = await apiService(`${baseUrl}store/apis/shippingcountries/`, {
|
|
7
|
+
method: "GET",
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
return responseData;
|
|
11
|
+
} catch (error) {
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const getPaymentMethods = async () => {
|
|
17
|
+
try {
|
|
18
|
+
const baseUrl = getBaseUrl();
|
|
19
|
+
const responseData = await apiService(`${baseUrl}store/apis/payment-methods/`, {
|
|
20
|
+
method: "GET",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return responseData;
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const getAvailablePaymentMethods = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const baseUrl = getBaseUrl();
|
|
32
|
+
const responseData = await apiService(`${baseUrl}store/apis/available-payment-methods/`, {
|
|
33
|
+
method: "GET",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return responseData;
|
|
37
|
+
} catch (error) {
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
getCountries,
|
|
44
|
+
getPaymentMethods,
|
|
45
|
+
getAvailablePaymentMethods,
|
|
46
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { apiService, getBaseUrl } from './ApiServices';
|
|
2
|
+
|
|
3
|
+
const addToWishlist = async (productId) => {
|
|
4
|
+
try {
|
|
5
|
+
const baseUrl = getBaseUrl();
|
|
6
|
+
const responseData = await apiService(`${baseUrl}store/apis/wishlist/`, {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
body: JSON.stringify({ product_id: productId, action: "add" }),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
return responseData;
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('failed to add to wishlist:', error);
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const removeFromWishlist = async (productId) => {
|
|
19
|
+
try {
|
|
20
|
+
const baseUrl = getBaseUrl();
|
|
21
|
+
const responseData = await apiService(`${baseUrl}store/apis/wishlist/`, {
|
|
22
|
+
method: 'POST',
|
|
23
|
+
body: JSON.stringify({ product_id: productId, action: "remove" }),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return responseData;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error('failed to remove from wishlist:', error);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const getWishlists = async () => {
|
|
34
|
+
try {
|
|
35
|
+
const baseUrl = getBaseUrl();
|
|
36
|
+
const responseData = await apiService(`${baseUrl}store/apis/wishlist/`, {
|
|
37
|
+
method: 'GET',
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return responseData;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('failed to get wishlist:', error);
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default {
|
|
48
|
+
addToWishlist,
|
|
49
|
+
getWishlists,
|
|
50
|
+
removeFromWishlist,
|
|
51
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { apiService } from "./ApiServices";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Page builder read API — store/apis/theme/…
|
|
5
|
+
*
|
|
6
|
+
* Every method takes `{ baseUrl, host }` explicitly rather than reaching for
|
|
7
|
+
* getBaseUrl() internally. On the server, Nuxt's async context is lost across
|
|
8
|
+
* every await, so a service that calls useState()/useRequestHeaders() after one
|
|
9
|
+
* throws "A composable that requires access to the Nuxt instance…".
|
|
10
|
+
*
|
|
11
|
+
* The caller resolves both while it still has context and passes them down.
|
|
12
|
+
* See useStorefront().load().
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function withQuery(url, params = {}) {
|
|
16
|
+
const search = new URLSearchParams();
|
|
17
|
+
for (const [key, value] of Object.entries(params)) {
|
|
18
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
19
|
+
search.append(key, value);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const qs = search.toString();
|
|
23
|
+
return qs ? `${url}?${qs}` : url;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function endpoint(baseUrl, path) {
|
|
27
|
+
if (!baseUrl) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
"storefront.service: no baseUrl. Resolve it with getBaseUrl() in setup " +
|
|
30
|
+
"or before your first await, then pass it in."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
// Tolerate a base URL with or without its trailing slash.
|
|
34
|
+
return `${baseUrl.replace(/\/+$/, "")}/${path}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const storefrontService = {
|
|
38
|
+
bootstrap: ({ baseUrl, host } = {}) =>
|
|
39
|
+
apiService(endpoint(baseUrl, "store/apis/theme/bootstrap/"), { host }),
|
|
40
|
+
|
|
41
|
+
render: ({ baseUrl, host, path } = {}) =>
|
|
42
|
+
apiService(
|
|
43
|
+
withQuery(endpoint(baseUrl, "store/apis/theme/render/"), { path }),
|
|
44
|
+
{ host }
|
|
45
|
+
),
|
|
46
|
+
|
|
47
|
+
preview: ({ baseUrl, host, theme, template, suffix, token } = {}) =>
|
|
48
|
+
apiService(
|
|
49
|
+
withQuery(endpoint(baseUrl, "store/apis/theme/preview/"), {
|
|
50
|
+
theme,
|
|
51
|
+
template,
|
|
52
|
+
suffix,
|
|
53
|
+
preview_token: token,
|
|
54
|
+
}),
|
|
55
|
+
{ host }
|
|
56
|
+
),
|
|
57
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category trees, one way.
|
|
3
|
+
*
|
|
4
|
+
* The categories endpoint nests children under a key that isn't the same
|
|
5
|
+
* everywhere (`children`, `sub_categories`, `subcategories`,
|
|
6
|
+
* `child_categories`). Every place that walks the tree reads it through here,
|
|
7
|
+
* so a subcategory is found no matter which one the API used.
|
|
8
|
+
*
|
|
9
|
+
* Flattened rows are stamped with where they sat in the tree (_depth,
|
|
10
|
+
* _parentId). A nested child often has no `parent` field of its own — its
|
|
11
|
+
* parent is implied by where it's nested — and without the stamp "top-level
|
|
12
|
+
* only" couldn't tell it apart from a root.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const CHILD_KEYS = ["children", "sub_categories", "subcategories", "child_categories"];
|
|
16
|
+
|
|
17
|
+
export function childrenOf(row) {
|
|
18
|
+
if (!row || typeof row !== "object") return [];
|
|
19
|
+
for (const key of CHILD_KEYS) {
|
|
20
|
+
if (Array.isArray(row[key]) && row[key].length) return row[key];
|
|
21
|
+
}
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The parent's id, from the row itself or from where it was nested. */
|
|
26
|
+
export function parentIdOf(row) {
|
|
27
|
+
if (!row) return null;
|
|
28
|
+
if (row._parentId != null) return row._parentId;
|
|
29
|
+
const p = row.parent ?? row.parent_id ?? row.parent_category ?? null;
|
|
30
|
+
if (p == null || p === "") return null;
|
|
31
|
+
return typeof p === "object" ? p.id ?? null : p;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isTopLevel(row) {
|
|
35
|
+
return parentIdOf(row) == null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Depth-first: each parent followed by its children. Deduplicated by id, so a
|
|
40
|
+
* payload that lists every category flat AND nests children under their parent
|
|
41
|
+
* doesn't show each child twice.
|
|
42
|
+
*/
|
|
43
|
+
export function flattenCategories(rows) {
|
|
44
|
+
const out = [];
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const walk = (list, depth, parentId) => {
|
|
47
|
+
for (const row of list ?? []) {
|
|
48
|
+
if (!row || typeof row !== "object") continue;
|
|
49
|
+
const key = row.id ?? row.slugified_name;
|
|
50
|
+
if (key != null && seen.has(key)) continue;
|
|
51
|
+
if (key != null) seen.add(key);
|
|
52
|
+
out.push(
|
|
53
|
+
depth === 0 && row._parentId === undefined
|
|
54
|
+
? row
|
|
55
|
+
: { ...row, _depth: depth, _parentId: row._parentId ?? parentId }
|
|
56
|
+
);
|
|
57
|
+
walk(childrenOf(row), depth + 1, row.id ?? null);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
walk(rows, 0, null);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Settings arrive as true / false / "true" / "false" depending on the path. */
|
|
65
|
+
export function isOn(value, fallback) {
|
|
66
|
+
if (value === undefined || value === null || value === "") return fallback;
|
|
67
|
+
return value === true || value === "true" || value === 1 || value === "1";
|
|
68
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared vocabulary for the theme's BUTTONS and INPUTS groups.
|
|
3
|
+
*
|
|
4
|
+
* Every storefront control reads the same CSS variables:
|
|
5
|
+
*
|
|
6
|
+
* --osi-button-* theme-level button look (useThemeTokens → .osi-root)
|
|
7
|
+
* --osi-input-* theme-level field look (useThemeTokens → .osi-root)
|
|
8
|
+
* --btn-* ONE button's own overrides (useButtonStyle → inline)
|
|
9
|
+
* --in-* ONE field's own overrides (useInputStyle → inline)
|
|
10
|
+
*
|
|
11
|
+
* assets/css/osi-controls.css resolves them in that order:
|
|
12
|
+
*
|
|
13
|
+
* individual (--btn-* / --in-*) → theme (--osi-button-* / --osi-input-*)
|
|
14
|
+
* → colour scheme (--osi-btn-bg, --osi-border…) → hard default
|
|
15
|
+
*
|
|
16
|
+
* so a section that sets its own radius wins, a section that doesn't follows
|
|
17
|
+
* the theme, and a theme that doesn't set a colour follows the scheme.
|
|
18
|
+
*
|
|
19
|
+
* Unset means "emit nothing": an undefined value never becomes a variable, so
|
|
20
|
+
* the next level down shows through instead of being pinned to a default.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
export const SHADOWS = {
|
|
24
|
+
NONE: "none",
|
|
25
|
+
SMALL: "0 1px 2px rgba(0,0,0,.12)",
|
|
26
|
+
MEDIUM: "0 4px 10px -2px rgba(0,0,0,.16)",
|
|
27
|
+
LARGE: "0 12px 24px -6px rgba(0,0,0,.22)",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const FONT_WEIGHTS = { LIGHT: 300, REGULAR: 400, MEDIUM: 500, SEMIBOLD: 600, BOLD: 700 };
|
|
31
|
+
export const TEXT_TRANSFORMS = {
|
|
32
|
+
NONE: "none",
|
|
33
|
+
UPPERCASE: "uppercase",
|
|
34
|
+
LOWERCASE: "lowercase",
|
|
35
|
+
CAPITALIZE: "capitalize",
|
|
36
|
+
};
|
|
37
|
+
export const FONT_FAMILIES = { HEADING: "var(--osi-font-heading)", BODY: "var(--osi-font-body)" };
|
|
38
|
+
|
|
39
|
+
export const BUTTON_STYLES = ["SOLID", "OUTLINE", "UNDERLINE", "TEXT"];
|
|
40
|
+
export const BUTTON_HOVERS = ["FADE", "DARKEN", "INVERT", "LIFT", "NONE"];
|
|
41
|
+
export const INPUT_STYLES = ["OUTLINE", "FILLED", "UNDERLINE"];
|
|
42
|
+
export const LABEL_STYLES = ["FLOATING", "ABOVE", "PLACEHOLDER"];
|
|
43
|
+
|
|
44
|
+
/** Empty string, null and undefined mean "not set" — 0 and false do not. */
|
|
45
|
+
export function has(value) {
|
|
46
|
+
return value !== undefined && value !== null && value !== "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function px(value) {
|
|
50
|
+
return has(value) && Number.isFinite(Number(value)) ? `${Number(value)}px` : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function color(value) {
|
|
54
|
+
return has(value) ? String(value) : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Picks value when it's one of allowed, else undefined (= inherit). */
|
|
58
|
+
export function pick(value, allowed) {
|
|
59
|
+
return allowed.includes(value) ? value : undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* BUTTON_SHADOW started life as a BOOLEAN. Old themes still store true/false,
|
|
64
|
+
* new ones store a SHADOWS key; both resolve here.
|
|
65
|
+
*/
|
|
66
|
+
export function shadow(value, shadowColor) {
|
|
67
|
+
if (value === true || value === "true") value = "SMALL";
|
|
68
|
+
if (value === false || value === "false") value = "NONE";
|
|
69
|
+
const preset = SHADOWS[value];
|
|
70
|
+
if (!preset) return undefined;
|
|
71
|
+
if (!shadowColor || preset === "none") return preset;
|
|
72
|
+
// A colour override swaps the rgba() in the preset for the merchant's.
|
|
73
|
+
return preset.replace(/rgba\([^)]*\)/, shadowColor);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function weight(value) {
|
|
77
|
+
if (!has(value)) return undefined;
|
|
78
|
+
return FONT_WEIGHTS[value] ?? (Number.isFinite(Number(value)) ? Number(value) : undefined);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function letterSpacing(value) {
|
|
82
|
+
return has(value) && Number.isFinite(Number(value)) ? `${Number(value) / 100}em` : undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Drops undefined entries so nothing unset reaches the DOM. */
|
|
86
|
+
export function compact(object) {
|
|
87
|
+
return Object.fromEntries(Object.entries(object).filter(([, v]) => v !== undefined));
|
|
88
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Section type -> component.
|
|
3
|
+
*
|
|
4
|
+
* The map is generated at build time by nuxt/osi-sections.ts from the theme's
|
|
5
|
+
* `app/sections/*Section.vue` (every layer is scanned; the store's theme wins):
|
|
6
|
+
*
|
|
7
|
+
* app/sections/HeroSection.vue -> HERO
|
|
8
|
+
* app/sections/LookbookSection.vue -> LOOKBOOK (a store's own type)
|
|
9
|
+
*
|
|
10
|
+
* Adding a section is one file and no registry edit.
|
|
11
|
+
*
|
|
12
|
+
* Naming is the contract:
|
|
13
|
+
*
|
|
14
|
+
* HeroSection.vue -> HERO
|
|
15
|
+
* ProductListSection.vue -> PRODUCT_LIST
|
|
16
|
+
* ClassificationListSection.vue -> CLASSIFICATION_LIST
|
|
17
|
+
* CustomHtmlSection.vue -> CUSTOM_HTML
|
|
18
|
+
*
|
|
19
|
+
* The type must match the backend's SectionType (or the store's own section
|
|
20
|
+
* type) exactly.
|
|
21
|
+
*/
|
|
22
|
+
import { sectionLoaders } from "#build/osi-sections.mjs";
|
|
23
|
+
|
|
24
|
+
export const SECTION_COMPONENTS = Object.fromEntries(
|
|
25
|
+
Object.entries(sectionLoaders).map(([type, loader]) => [type, defineAsyncComponent(loader)])
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const warned = new Set();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Unknown types render nothing rather than a broken box. A merchant can author
|
|
32
|
+
* a custom section type (StoreSection backend-side) with no component here yet,
|
|
33
|
+
* and a blank gap beats a visible error on a live storefront.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveSection(type) {
|
|
36
|
+
const component = SECTION_COMPONENTS[type];
|
|
37
|
+
if (component) return component;
|
|
38
|
+
|
|
39
|
+
if (import.meta.dev && !warned.has(type)) {
|
|
40
|
+
warned.add(type);
|
|
41
|
+
console.warn(
|
|
42
|
+
`[storefront] No component for section type "${type}". ` +
|
|
43
|
+
`Create app/sections/${pascal(type)}Section.vue in the theme.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** MEDIA_WITH_TEXT -> MediaWithText, for the dev warning above. */
|
|
50
|
+
function pascal(type) {
|
|
51
|
+
return String(type)
|
|
52
|
+
.toLowerCase()
|
|
53
|
+
.split("_")
|
|
54
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
55
|
+
.join("");
|
|
56
|
+
}
|