@mohasinac/appkit 2.7.17 → 2.7.19
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/_internal/server/features/bundles/actions.d.ts +15 -0
- package/dist/_internal/server/features/bundles/actions.js +48 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -0
- package/dist/_internal/server/features/bundles/index.js +1 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +4 -0
- package/dist/_internal/server/features/checkout/actions.js +23 -12
- package/dist/_internal/server/features/payments/data.d.ts +5 -3
- package/dist/_internal/server/features/payments/data.js +13 -8
- package/dist/_internal/server/features/payments/index.d.ts +1 -1
- package/dist/_internal/server/features/payments/index.js +1 -1
- package/dist/_internal/server/features/seo/manifest.js +9 -3
- package/dist/_internal/shared/features/categories/bundle-config.d.ts +24 -0
- package/dist/_internal/shared/features/categories/bundle-config.js +28 -0
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +7 -0
- package/dist/_internal/shared/features/categories/bundle-copy.js +9 -0
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +69 -19
- package/dist/_internal/shared/features/categories/bundle-schemas.js +20 -2
- package/dist/_internal/shared/features/checkout/config.d.ts +1 -1
- package/dist/_internal/shared/features/checkout/config.js +1 -1
- package/dist/_internal/shared/features/payments/config.d.ts +2 -1
- package/dist/_internal/shared/features/payments/config.js +2 -1
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/constants/field-names.d.ts +8 -1
- package/dist/constants/field-names.js +9 -2
- package/dist/constants/table-keys.d.ts +3 -0
- package/dist/constants/table-keys.js +3 -0
- package/dist/features/admin/components/AdminSiteSettingsView.js +17 -15
- package/dist/features/admin/components/DashboardStats.js +20 -12
- package/dist/features/admin/components/analytics/AdminAnalyticsCharts.js +18 -2
- package/dist/features/admin/components/analytics/AdminStatCard.d.ts +7 -1
- package/dist/features/admin/components/analytics/AdminStatCard.js +13 -2
- package/dist/features/admin/schemas/firestore.d.ts +39 -4
- package/dist/features/admin/schemas/firestore.js +14 -1
- package/dist/features/categories/components/BundleBuyNowCta.d.ts +3 -1
- package/dist/features/categories/components/BundleBuyNowCta.js +3 -3
- package/dist/features/categories/components/BundleDynamicRuleEditor.d.ts +1 -1
- package/dist/features/categories/components/BundleDynamicRuleEditor.js +2 -1
- package/dist/features/categories/components/BundlesListView.d.ts +4 -1
- package/dist/features/categories/components/BundlesListView.js +2 -2
- package/dist/features/categories/components/CategoryBundlesListing.d.ts +4 -1
- package/dist/features/categories/components/CategoryBundlesListing.js +9 -8
- package/dist/features/categories/schemas/firestore.d.ts +29 -1
- package/dist/features/checkout/actions/checkout-actions.d.ts +11 -0
- package/dist/features/checkout/actions/checkout-actions.js +26 -0
- package/dist/features/filters/FilterFacetSection.d.ts +6 -1
- package/dist/features/filters/FilterFacetSection.js +25 -2
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +4 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +9 -8
- package/dist/features/orders/actions/refund-actions.js +10 -7
- package/dist/features/orders/schemas/firestore.d.ts +16 -13
- package/dist/features/orders/schemas/firestore.js +6 -5
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -1
- package/dist/features/products/components/AuctionsIndexListing.js +2 -1
- package/dist/features/products/components/ProductDetailPageView.js +3 -1
- package/dist/features/products/components/ProductDetailView.d.ts +3 -1
- package/dist/features/products/components/ProductDetailView.js +2 -2
- package/dist/features/products/components/ProductFilters.d.ts +10 -4
- package/dist/features/products/components/ProductFilters.js +20 -16
- package/dist/features/products/components/ProductGrid.js +2 -2
- package/dist/features/products/components/ProductsIndexListing.js +2 -1
- package/dist/features/products/constants/action-defs.d.ts +64 -22
- package/dist/features/products/constants/action-defs.js +101 -64
- package/dist/features/products/constants/product-features.constants.js +5 -1
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/products/schemas/product-features.d.ts +1 -1
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/products/schemas/product-features.validators.js +4 -0
- package/dist/features/seller/components/SellerCouponEditorView.js +2 -2
- package/dist/features/seller/components/analytics/SellerAnalyticsStats.js +10 -4
- package/dist/features/stores/components/StoreBundlesPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreBundlesPageView.js +2 -2
- package/dist/features/stores/components/StoresIndexListing.js +2 -1
- package/dist/providers/db-firebase/admin-app-lite.js +8 -2
- package/dist/providers/db-firebase/admin.js +8 -2
- package/dist/seed/addresses-seed-data.js +24 -7
- package/dist/seed/cart-seed-data.d.ts +10 -10
- package/dist/seed/cart-seed-data.js +39 -2
- package/dist/seed/grouped-listings-seed-data.js +47 -0
- package/dist/seed/orders-seed-data.js +150 -0
- package/dist/seed/products-standard-seed-data.js +42 -0
- package/dist/seed/site-settings-seed-data.js +14 -4
- package/dist/seed/users-seed-data.js +1 -1
- package/dist/seed/wishlists-seed-data.js +7 -0
- package/dist/server-entry.d.ts +2 -1
- package/dist/server-entry.js +2 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +3 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/SideDrawer.style.css +49 -11
- package/package.json +1 -1
|
@@ -232,15 +232,27 @@ export interface SiteSettingsDocument {
|
|
|
232
232
|
codEnabled: boolean;
|
|
233
233
|
};
|
|
234
234
|
commissions: {
|
|
235
|
-
|
|
235
|
+
/** Our platform cut charged to the buyer as a % of order value (e.g. 5 = 5%). */
|
|
236
|
+
platformFeePercent: number;
|
|
237
|
+
/** GST applied on top of our platform fee (18%). buyer pays platformFee × (1 + gstPercent/100). */
|
|
238
|
+
gstPercent: number;
|
|
239
|
+
/** Per-transaction gateway minimum fee in rupees. Ensures total charge is never below this floor. 0 = no minimum. */
|
|
240
|
+
minimumTransactionFee: number;
|
|
241
|
+
/** Razorpay gateway cost % (absorbed by platform, not passed through separately). */
|
|
242
|
+
gatewayFeePercent: number;
|
|
236
243
|
codDepositPercent: number;
|
|
237
244
|
sellerShippingFixed: number;
|
|
238
245
|
platformShippingPercent: number;
|
|
239
246
|
platformShippingFixedMin: number;
|
|
240
|
-
processingFeePercent?: number;
|
|
241
|
-
gstPercent?: number;
|
|
242
|
-
gatewayFeePercent?: number;
|
|
243
247
|
autoPayoutWindowDays?: number;
|
|
248
|
+
/** Days before a settled order's funds are released for payout. */
|
|
249
|
+
payoutHoldDays?: number;
|
|
250
|
+
/** Minimum payout amount in rupees. */
|
|
251
|
+
minPayoutAmount?: number;
|
|
252
|
+
auctionListingFee?: number;
|
|
253
|
+
preOrderListingFee?: number;
|
|
254
|
+
featuredSlotFee?: number;
|
|
255
|
+
promotedSlotFee?: number;
|
|
244
256
|
};
|
|
245
257
|
socialLinks: {
|
|
246
258
|
facebook?: string;
|
|
@@ -280,6 +292,8 @@ export interface SiteSettingsDocument {
|
|
|
280
292
|
sellerRegistration: boolean;
|
|
281
293
|
preOrders: boolean;
|
|
282
294
|
seedPanel: boolean;
|
|
295
|
+
/** When true, admin users see a bypass button in checkout that skips OTP and payment. Server-enforced. */
|
|
296
|
+
adminCheckoutBypass?: boolean;
|
|
283
297
|
listingTypes?: {
|
|
284
298
|
standard?: boolean;
|
|
285
299
|
auction?: boolean;
|
|
@@ -371,6 +385,27 @@ export interface SiteSettingsDocument {
|
|
|
371
385
|
accentDark?: string;
|
|
372
386
|
};
|
|
373
387
|
featuredResults?: FeaturedResult[];
|
|
388
|
+
/**
|
|
389
|
+
* Per-action runtime enable/disable overrides.
|
|
390
|
+
* Key = ActionId value string (e.g. "checkout", "add-to-wishlist").
|
|
391
|
+
* Absent key = action is enabled by default.
|
|
392
|
+
*/
|
|
393
|
+
actionConfig?: Partial<Record<string, {
|
|
394
|
+
enabled: boolean;
|
|
395
|
+
}>>;
|
|
396
|
+
/**
|
|
397
|
+
* Per-nav-item runtime enable/disable overrides.
|
|
398
|
+
* Key = NavItem.id (nav-* slug, e.g. "nav-products").
|
|
399
|
+
* Absent key = nav item is enabled by default.
|
|
400
|
+
*/
|
|
401
|
+
navConfig?: Record<string, {
|
|
402
|
+
enabled: boolean;
|
|
403
|
+
}>;
|
|
404
|
+
/**
|
|
405
|
+
* Derived hrefs of disabled nav items — written by updateNavConfigAction
|
|
406
|
+
* alongside navConfig. Read by RSC public layouts to block disabled routes.
|
|
407
|
+
*/
|
|
408
|
+
disabledRoutes?: string[];
|
|
374
409
|
createdAt: Date;
|
|
375
410
|
updatedAt: Date;
|
|
376
411
|
}
|
|
@@ -136,11 +136,21 @@ export const DEFAULT_SITE_SETTINGS_DATA = {
|
|
|
136
136
|
codEnabled: true,
|
|
137
137
|
},
|
|
138
138
|
commissions: {
|
|
139
|
-
|
|
139
|
+
platformFeePercent: 5,
|
|
140
|
+
gstPercent: 18,
|
|
141
|
+
minimumTransactionFee: 0,
|
|
142
|
+
gatewayFeePercent: 2,
|
|
140
143
|
codDepositPercent: 10,
|
|
141
144
|
sellerShippingFixed: 0,
|
|
142
145
|
platformShippingPercent: 10,
|
|
143
146
|
platformShippingFixedMin: 0,
|
|
147
|
+
autoPayoutWindowDays: 7,
|
|
148
|
+
payoutHoldDays: 2,
|
|
149
|
+
minPayoutAmount: 100,
|
|
150
|
+
auctionListingFee: 0,
|
|
151
|
+
preOrderListingFee: 0,
|
|
152
|
+
featuredSlotFee: 999,
|
|
153
|
+
promotedSlotFee: 499,
|
|
144
154
|
},
|
|
145
155
|
featureFlags: {
|
|
146
156
|
chats: true,
|
|
@@ -186,6 +196,9 @@ export const DEFAULT_SITE_SETTINGS_DATA = {
|
|
|
186
196
|
enabled: true,
|
|
187
197
|
message: "🎉 Up to 15% Off on Pokémon TCG this week — Use code SAVE15",
|
|
188
198
|
},
|
|
199
|
+
actionConfig: {},
|
|
200
|
+
navConfig: {},
|
|
201
|
+
disabledRoutes: [],
|
|
189
202
|
};
|
|
190
203
|
export const SITE_SETTINGS_PUBLIC_FIELDS = [
|
|
191
204
|
"siteName",
|
|
@@ -4,5 +4,7 @@ export interface BundleBuyNowCtaProps {
|
|
|
4
4
|
onBuyNow: (input: {
|
|
5
5
|
bundleSlug: string;
|
|
6
6
|
}) => Promise<unknown>;
|
|
7
|
+
/** Renders a smaller single-line button suited for list cards. */
|
|
8
|
+
compact?: boolean;
|
|
7
9
|
}
|
|
8
|
-
export declare function BundleBuyNowCta({ bundleSlug, outOfStock, onBuyNow, }: BundleBuyNowCtaProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare function BundleBuyNowCta({ bundleSlug, outOfStock, onBuyNow, compact, }: BundleBuyNowCtaProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -12,7 +12,7 @@ import { Button, LoginRequiredModal, Stack, Text } from "../../../ui";
|
|
|
12
12
|
import { useToast } from "../../../ui";
|
|
13
13
|
import { isAuthError } from "../../../utils/auth-error";
|
|
14
14
|
import { BUNDLE_COPY } from "../../../_internal/shared/features/categories/bundle-copy";
|
|
15
|
-
export function BundleBuyNowCta({ bundleSlug, outOfStock = false, onBuyNow, }) {
|
|
15
|
+
export function BundleBuyNowCta({ bundleSlug, outOfStock = false, onBuyNow, compact = false, }) {
|
|
16
16
|
const { showToast } = useToast();
|
|
17
17
|
const [submitting, setSubmitting] = useState(false);
|
|
18
18
|
const [error, setError] = useState(null);
|
|
@@ -38,7 +38,7 @@ export function BundleBuyNowCta({ bundleSlug, outOfStock = false, onBuyNow, }) {
|
|
|
38
38
|
}
|
|
39
39
|
}, [bundleSlug, onBuyNow, showToast]);
|
|
40
40
|
if (outOfStock) {
|
|
41
|
-
return (_jsxs(Stack, { gap: "xs", "aria-live": "polite", children: [_jsx(Button, { variant: "primary", disabled: true, "aria-disabled": true, children: BUNDLE_COPY.detail.ctaOutOfStock }), _jsx(Text, { size: "xs", color: "muted", children: BUNDLE_COPY.detail.ctaHint })] }));
|
|
41
|
+
return (_jsxs(Stack, { gap: "xs", "aria-live": "polite", children: [_jsx(Button, { variant: "primary", size: compact ? "sm" : "md", disabled: true, "aria-disabled": true, className: compact ? "w-full" : undefined, children: BUNDLE_COPY.detail.ctaOutOfStock }), !compact && (_jsx(Text, { size: "xs", color: "muted", children: BUNDLE_COPY.detail.ctaHint }))] }));
|
|
42
42
|
}
|
|
43
|
-
return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to purchase this bundle. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", onClick: handleClick, disabled: submitting, "aria-busy": submitting, children: submitting ? BUNDLE_COPY.detail.ctaAdding : BUNDLE_COPY.detail.ctaBuyNow }), error && (_jsx(Text, { size: "sm", color: "danger", role: "alert", children: error }))] })] }));
|
|
43
|
+
return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to purchase this bundle. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: compact ? "sm" : "md", onClick: handleClick, disabled: submitting, "aria-busy": submitting, className: compact ? "w-full" : undefined, children: submitting ? BUNDLE_COPY.detail.ctaAdding : BUNDLE_COPY.detail.ctaBuyNow }), error && (_jsx(Text, { size: "sm", color: "danger", role: "alert", children: error }))] })] }));
|
|
44
44
|
}
|
|
@@ -4,7 +4,7 @@ type DynamicRule = {
|
|
|
4
4
|
categorySlug?: string;
|
|
5
5
|
brandSlug?: string;
|
|
6
6
|
tags?: string[];
|
|
7
|
-
listingType?: "standard" | "pre-order";
|
|
7
|
+
listingType?: "standard" | "pre-order" | "prize-draw";
|
|
8
8
|
};
|
|
9
9
|
orderBy?: "price-asc" | "price-desc" | "createdAt-desc";
|
|
10
10
|
limit: number;
|
|
@@ -29,9 +29,10 @@ const ORDER_BY_OPTIONS = [
|
|
|
29
29
|
{ label: "Price (high → low)", value: "price-desc" },
|
|
30
30
|
];
|
|
31
31
|
const LISTING_TYPE_OPTIONS = [
|
|
32
|
-
{ label: "Any
|
|
32
|
+
{ label: "Any", value: "" },
|
|
33
33
|
{ label: "Standard only", value: "standard" },
|
|
34
34
|
{ label: "Pre-order only", value: "pre-order" },
|
|
35
|
+
{ label: "Prize draw only", value: "prize-draw" },
|
|
35
36
|
];
|
|
36
37
|
export function BundleDynamicRuleEditor({ value, onChange, disabled = false, }) {
|
|
37
38
|
const rule = value ?? DEFAULT_DYNAMIC_RULE;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
type SearchParams = Record<string, string | string[]>;
|
|
2
2
|
export interface BundlesListViewProps {
|
|
3
3
|
searchParams?: SearchParams;
|
|
4
|
+
onBuyNow?: (input: {
|
|
5
|
+
bundleSlug: string;
|
|
6
|
+
}) => Promise<unknown>;
|
|
4
7
|
}
|
|
5
8
|
/**
|
|
6
9
|
* Public `/bundles` index. Bundles live as `categoryType:"bundle"` rows on
|
|
@@ -9,5 +12,5 @@ export interface BundlesListViewProps {
|
|
|
9
12
|
* Optional `?storeId=…` post-filters to a single seller's bundles.
|
|
10
13
|
* Sort UI is owned by `CategoryBundlesListing` (client).
|
|
11
14
|
*/
|
|
12
|
-
export declare function BundlesListView({ searchParams, }: BundlesListViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
15
|
+
export declare function BundlesListView({ searchParams, onBuyNow, }: BundlesListViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
|
|
13
16
|
export {};
|
|
@@ -15,7 +15,7 @@ function sp(params, key) {
|
|
|
15
15
|
* Optional `?storeId=…` post-filters to a single seller's bundles.
|
|
16
16
|
* Sort UI is owned by `CategoryBundlesListing` (client).
|
|
17
17
|
*/
|
|
18
|
-
export async function BundlesListView({ searchParams = {}, }) {
|
|
18
|
+
export async function BundlesListView({ searchParams = {}, onBuyNow, }) {
|
|
19
19
|
const storeId = sp(searchParams, "storeId");
|
|
20
20
|
const all = await categoriesRepository
|
|
21
21
|
.listByType("bundle", { activeOnly: true, limit: DEFAULT_LIMIT })
|
|
@@ -23,5 +23,5 @@ export async function BundlesListView({ searchParams = {}, }) {
|
|
|
23
23
|
const bundles = storeId
|
|
24
24
|
? all.filter((c) => c.createdByStoreId === storeId)
|
|
25
25
|
: all;
|
|
26
|
-
return (_jsx(Main, { children: _jsx(Section, { className: "py-10", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-2 text-3xl font-semibold text-zinc-900 dark:text-zinc-100", children: "Collectible Bundles" }), _jsx(Text, { className: "mb-6 text-sm text-[var(--appkit-color-text-muted)]", children: "Curated multi-product bundles \u2014 Pok\u00E9mon starter sets, Beyblade tournament packs, Hot Wheels collector boxes and more. One price, one checkout, one shipment." }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(CategoryBundlesListing, { initialBundles: bundles }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
|
|
26
|
+
return (_jsx(Main, { children: _jsx(Section, { className: "py-10", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-2 text-3xl font-semibold text-zinc-900 dark:text-zinc-100", children: "Collectible Bundles" }), _jsx(Text, { className: "mb-6 text-sm text-[var(--appkit-color-text-muted)]", children: "Curated multi-product bundles \u2014 Pok\u00E9mon starter sets, Beyblade tournament packs, Hot Wheels collector boxes and more. One price, one checkout, one shipment." }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(CategoryBundlesListing, { initialBundles: bundles, onBuyNow: onBuyNow }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
|
|
27
27
|
}
|
|
@@ -2,5 +2,8 @@ import type { CategoryDocument } from "../schemas";
|
|
|
2
2
|
export interface CategoryBundlesListingProps {
|
|
3
3
|
initialBundles: CategoryDocument[];
|
|
4
4
|
brandName?: string;
|
|
5
|
+
onBuyNow?: (input: {
|
|
6
|
+
bundleSlug: string;
|
|
7
|
+
}) => Promise<unknown>;
|
|
5
8
|
}
|
|
6
|
-
export declare function CategoryBundlesListing({ initialBundles, brandName, }: CategoryBundlesListingProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export declare function CategoryBundlesListing({ initialBundles, brandName, onBuyNow, }: CategoryBundlesListingProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -12,6 +12,7 @@ import Link from "next/link";
|
|
|
12
12
|
import { Badge, Div, Row, Select, Stack, Text } from "../../../ui";
|
|
13
13
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
14
14
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
15
|
+
import { BundleBuyNowCta } from "./BundleBuyNowCta";
|
|
15
16
|
const SORT_OPTIONS = [
|
|
16
17
|
{ label: "Newest", value: "newest" },
|
|
17
18
|
{ label: "Price ↑", value: "price-asc" },
|
|
@@ -47,7 +48,7 @@ function comparator(sort) {
|
|
|
47
48
|
return bT - aT;
|
|
48
49
|
};
|
|
49
50
|
}
|
|
50
|
-
export function CategoryBundlesListing({ initialBundles, brandName, }) {
|
|
51
|
+
export function CategoryBundlesListing({ initialBundles, brandName, onBuyNow, }) {
|
|
51
52
|
const [sort, setSort] = useState("newest");
|
|
52
53
|
const filtered = useMemo(() => initialBundles
|
|
53
54
|
.filter((c) => c.categoryType === "bundle")
|
|
@@ -56,17 +57,17 @@ export function CategoryBundlesListing({ initialBundles, brandName, }) {
|
|
|
56
57
|
if (filtered.length === 0) {
|
|
57
58
|
return (_jsx(Div, { className: "rounded-2xl border border-dashed border-zinc-200 py-16 text-center dark:border-zinc-700", children: _jsxs(Text, { color: "muted", children: [COPY.empty, brandName ? COPY.forBrand(brandName) : "", COPY.yetSuffix] }) }));
|
|
58
59
|
}
|
|
59
|
-
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "end", children: [_jsx(Text, { size: "sm", color: "muted", children: COPY.sortLabel }), _jsx(Select, { options: SORT_OPTIONS, value: sort, onValueChange: setSort, "aria-label": COPY.sortLabel })] }), _jsx(Div, { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: filtered.map((bundle) => (_jsx(BundleCard, { bundle: bundle }, bundle.id))) })] }));
|
|
60
|
+
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "end", children: [_jsx(Text, { size: "sm", color: "muted", children: COPY.sortLabel }), _jsx(Select, { options: SORT_OPTIONS, value: sort, onValueChange: setSort, "aria-label": COPY.sortLabel })] }), _jsx(Div, { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: filtered.map((bundle) => (_jsx(BundleCard, { bundle: bundle, onBuyNow: onBuyNow }, bundle.id))) })] }));
|
|
60
61
|
}
|
|
61
|
-
function BundleCard({ bundle }) {
|
|
62
|
+
function BundleCard({ bundle, onBuyNow }) {
|
|
62
63
|
const memberCount = bundle.bundleProductIds?.length ?? 0;
|
|
63
64
|
const stock = bundle.bundleStockStatus ?? "in_stock";
|
|
64
65
|
const badge = STOCK_BADGE_TEXT[stock];
|
|
65
66
|
const cover = bundle.display?.coverImage;
|
|
66
67
|
const href = String(ROUTES.PUBLIC.BUNDLE_DETAIL?.(bundle.slug) ?? "#");
|
|
67
|
-
return (_jsxs(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
return (_jsxs(Stack, { gap: "none", className: "rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900", children: [_jsxs(Link, { href: href, className: "group block p-3 hover:no-underline", children: [_jsx(Div, { className: "mb-2 aspect-video overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800", children: cover ? (
|
|
69
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
70
|
+
_jsx("img", { src: cover, alt: bundle.name, className: "h-full w-full object-cover transition-transform group-hover:scale-105", loading: "lazy" })) : (_jsx(Div, { className: "flex h-full w-full items-center justify-center text-3xl", children: PLACEHOLDER_EMOJI })) }), _jsx(Text, { className: "line-clamp-2 text-sm font-semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", children: [_jsx(Text, { size: "sm", weight: "bold", children: bundle.bundlePriceInPaise
|
|
71
|
+
? formatCurrency(bundle.bundlePriceInPaise / 100, "INR")
|
|
72
|
+
: COPY.priceFallback }), _jsx(Text, { size: "xs", color: "muted", children: COPY.items(memberCount) })] }), badge && (_jsx(Badge, { variant: STOCK_BADGE_VARIANT[stock], className: "mt-1", children: badge }))] }), onBuyNow && (_jsx(Div, { className: "border-t border-zinc-100 p-3 pt-2 dark:border-zinc-800", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }));
|
|
72
73
|
}
|
|
@@ -27,11 +27,24 @@ export type BundleQueryRule = {
|
|
|
27
27
|
categorySlug?: string;
|
|
28
28
|
brandSlug?: string;
|
|
29
29
|
tags?: string[];
|
|
30
|
-
|
|
30
|
+
/** Eligible listing types that may appear in a dynamic bundle. */
|
|
31
|
+
listingType?: "standard" | "pre-order" | "prize-draw";
|
|
31
32
|
};
|
|
32
33
|
orderBy?: "price-asc" | "price-desc" | "createdAt-desc";
|
|
33
34
|
limit: number;
|
|
34
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Per-member metadata stored alongside `bundleProductIds`.
|
|
38
|
+
* `drawCount` — only meaningful when the member product has
|
|
39
|
+
* `listingType === "prize-draw"`. Represents how many draw entries
|
|
40
|
+
* the buyer receives for that product when they purchase the bundle
|
|
41
|
+
* (e.g. drawCount=5 means 5 raffle entries, not 5 copies of the draw).
|
|
42
|
+
*/
|
|
43
|
+
export interface BundleItemDetail {
|
|
44
|
+
productId: string;
|
|
45
|
+
/** Number of draw entries included when product is a prize-draw. */
|
|
46
|
+
drawCount?: number;
|
|
47
|
+
}
|
|
35
48
|
/** Full Firestore category document (includes server-only fields) */
|
|
36
49
|
export interface CategoryDocumentSEO {
|
|
37
50
|
title: string;
|
|
@@ -100,6 +113,14 @@ export interface CategoryDocument {
|
|
|
100
113
|
brandFounded?: number;
|
|
101
114
|
/** Banner image for the brand storefront page — categoryType==="brand". */
|
|
102
115
|
brandBannerImage?: string;
|
|
116
|
+
/**
|
|
117
|
+
* "special" — admin-curated bundle; writes reverse `partOfBundleIds`
|
|
118
|
+
* pointers on member products and shows the "Bundled" badge on cards.
|
|
119
|
+
*
|
|
120
|
+
* "brand" — auto-generated brand collection (dynamic brandSlug query).
|
|
121
|
+
* Does NOT update `partOfBundleIds`; used as a discovery surface only.
|
|
122
|
+
*/
|
|
123
|
+
bundleKind?: "special" | "brand";
|
|
103
124
|
/** Discounted bundle price in paise. */
|
|
104
125
|
bundlePriceInPaise?: number;
|
|
105
126
|
/** Rule resolving the bundle's member products — static list or live query. */
|
|
@@ -110,6 +131,13 @@ export interface CategoryDocument {
|
|
|
110
131
|
bundleQueryResolvedAt?: Date;
|
|
111
132
|
/** Hand-picked products list (mirror of bundleQueryRule for static rules); kept for index-friendly queries. */
|
|
112
133
|
bundleProductIds?: string[];
|
|
134
|
+
/**
|
|
135
|
+
* Per-member metadata parallel to `bundleProductIds`.
|
|
136
|
+
* Carries `drawCount` for prize-draw members (how many raffle entries
|
|
137
|
+
* the buyer receives). Flat `bundleProductIds` is kept for Firestore
|
|
138
|
+
* array-contains queries; this array holds the richer shape.
|
|
139
|
+
*/
|
|
140
|
+
bundleItemDetails?: BundleItemDetail[];
|
|
113
141
|
seo: CategoryDocumentSEO;
|
|
114
142
|
display: CategoryDocumentDisplay;
|
|
115
143
|
isActive: boolean;
|
|
@@ -17,4 +17,15 @@ export declare function verifyCheckoutConsentOtp(userId: string, addressId: stri
|
|
|
17
17
|
* registered phone number.
|
|
18
18
|
*/
|
|
19
19
|
export declare function grantCheckoutConsentViaSms(userId: string, userPhone: string | undefined, addressId: string): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Grants checkout consent for an admin test order, bypassing the normal OTP flow.
|
|
22
|
+
*
|
|
23
|
+
* The resulting Firestore doc is identical to a verified consent OTP so that
|
|
24
|
+
* `createCheckoutOrderAction` can proceed without modification. The
|
|
25
|
+
* `verifiedVia: "admin_bypass"` marker prevents bypass-credit grants.
|
|
26
|
+
*
|
|
27
|
+
* Security: callers MUST verify the requesting user is an admin AND that
|
|
28
|
+
* `siteSettings.featureFlags.adminCheckoutBypass === true` before calling this.
|
|
29
|
+
*/
|
|
30
|
+
export declare function grantAdminCheckoutBypass(userId: string, addressId: string, bypassingAdminUid: string): Promise<void>;
|
|
20
31
|
export { userRepository };
|
|
@@ -94,4 +94,30 @@ export async function grantCheckoutConsentViaSms(userId, userPhone, addressId) {
|
|
|
94
94
|
});
|
|
95
95
|
serverLogger.info(`Checkout consent granted via SMS: uid=${userId} addressId=${addressId}`);
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Grants checkout consent for an admin test order, bypassing the normal OTP flow.
|
|
99
|
+
*
|
|
100
|
+
* The resulting Firestore doc is identical to a verified consent OTP so that
|
|
101
|
+
* `createCheckoutOrderAction` can proceed without modification. The
|
|
102
|
+
* `verifiedVia: "admin_bypass"` marker prevents bypass-credit grants.
|
|
103
|
+
*
|
|
104
|
+
* Security: callers MUST verify the requesting user is an admin AND that
|
|
105
|
+
* `siteSettings.featureFlags.adminCheckoutBypass === true` before calling this.
|
|
106
|
+
*/
|
|
107
|
+
export async function grantAdminCheckoutBypass(userId, addressId, bypassingAdminUid) {
|
|
108
|
+
const address = await findUserAddress(userId, addressId);
|
|
109
|
+
if (!address)
|
|
110
|
+
throw new ValidationError("Address not found.");
|
|
111
|
+
await saveConsentOtp(userId, addressId, {
|
|
112
|
+
codeHash: "",
|
|
113
|
+
expiresAt: new Date(Date.now() + CONSENT_OTP_EXPIRY_MS),
|
|
114
|
+
attempts: 0,
|
|
115
|
+
verified: true,
|
|
116
|
+
verifiedVia: "admin_bypass",
|
|
117
|
+
adminBypassBy: bypassingAdminUid,
|
|
118
|
+
addressId,
|
|
119
|
+
createdAt: new Date(),
|
|
120
|
+
});
|
|
121
|
+
serverLogger.info(`Admin checkout bypass granted: uid=${userId} addressId=${addressId} bypassedBy=${bypassingAdminUid}`);
|
|
122
|
+
}
|
|
97
123
|
export { userRepository };
|
|
@@ -15,10 +15,15 @@ export interface FilterFacetSectionProps {
|
|
|
15
15
|
onToggle?: () => void;
|
|
16
16
|
onClear?: () => void;
|
|
17
17
|
className?: string;
|
|
18
|
+
/**
|
|
19
|
+
* "accordion" (default) — collapsible inline section inside a filter drawer.
|
|
20
|
+
* "dropdown" — compact pill button with a floating panel; use in horizontal toolbars.
|
|
21
|
+
*/
|
|
22
|
+
displayAs?: "accordion" | "dropdown";
|
|
18
23
|
}
|
|
19
24
|
/**
|
|
20
25
|
* FilterFacetSection — collapsible checkbox/radio filter section.
|
|
21
26
|
* Supports single-select and multi-select modes.
|
|
22
27
|
* When searchable, the search input is always visible and typing auto-expands.
|
|
23
28
|
*/
|
|
24
|
-
export declare function FilterFacetSection({ title, options, selected, onChange, selectionMode, defaultCollapsed, searchable, isOpen: controlledOpen, onToggle, onClear, className, }: FilterFacetSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
29
|
+
export declare function FilterFacetSection({ title, options, selected, onChange, selectionMode, defaultCollapsed, searchable, isOpen: controlledOpen, onToggle, onClear, className, displayAs, }: FilterFacetSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from "react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { Button, Div, Input, Row, Span, Text } from "../../ui";
|
|
5
5
|
import { cn } from "./filterUtils";
|
|
6
6
|
/**
|
|
@@ -8,10 +8,23 @@ import { cn } from "./filterUtils";
|
|
|
8
8
|
* Supports single-select and multi-select modes.
|
|
9
9
|
* When searchable, the search input is always visible and typing auto-expands.
|
|
10
10
|
*/
|
|
11
|
-
export function FilterFacetSection({ title, options, selected, onChange, selectionMode = "multi", defaultCollapsed = true, searchable = false, isOpen: controlledOpen, onToggle, onClear, className = "", }) {
|
|
11
|
+
export function FilterFacetSection({ title, options, selected, onChange, selectionMode = "multi", defaultCollapsed = true, searchable = false, isOpen: controlledOpen, onToggle, onClear, className = "", displayAs = "accordion", }) {
|
|
12
12
|
const isControlled = controlledOpen !== undefined;
|
|
13
13
|
const [internalCollapsed, setInternalCollapsed] = useState(defaultCollapsed);
|
|
14
14
|
const [search, setSearch] = useState("");
|
|
15
|
+
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
16
|
+
const dropdownRef = useRef(null);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (displayAs !== "dropdown")
|
|
19
|
+
return;
|
|
20
|
+
function handleOutsideClick(e) {
|
|
21
|
+
if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
|
|
22
|
+
setDropdownOpen(false);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
document.addEventListener("mousedown", handleOutsideClick);
|
|
26
|
+
return () => document.removeEventListener("mousedown", handleOutsideClick);
|
|
27
|
+
}, [displayAs]);
|
|
15
28
|
const isCollapsed = isControlled ? !controlledOpen : internalCollapsed;
|
|
16
29
|
const handleToggle = () => {
|
|
17
30
|
if (onToggle)
|
|
@@ -44,6 +57,16 @@ export function FilterFacetSection({ title, options, selected, onChange, selecti
|
|
|
44
57
|
const selectedLabels = selected
|
|
45
58
|
.map((v) => options.find((o) => o.value === v)?.label ?? v)
|
|
46
59
|
.slice(0, 3);
|
|
60
|
+
// ── Dropdown variant ────────────────────────────────────────────────────
|
|
61
|
+
if (displayAs === "dropdown") {
|
|
62
|
+
return (_jsxs(Div, { ref: dropdownRef, className: cn("relative inline-block", className), children: [_jsxs(Button, { type: "button", onClick: () => setDropdownOpen((o) => !o), variant: "ghost", className: cn("inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors", hasValue
|
|
63
|
+
? "border-emerald-500 bg-emerald-50 text-emerald-700 dark:border-emerald-500/60 dark:bg-emerald-900/20 dark:text-emerald-300"
|
|
64
|
+
: "border-zinc-200 bg-white text-zinc-700 hover:border-zinc-400 dark:border-slate-700 dark:bg-slate-900 dark:text-zinc-200 dark:hover:border-slate-500"), children: [title, hasValue && (_jsx(Span, { className: "inline-flex h-4 w-4 items-center justify-center rounded-full bg-emerald-600 text-[10px] font-bold text-white", children: selected.length })), _jsx("svg", { className: cn("h-3.5 w-3.5 text-zinc-400 transition-transform", dropdownOpen ? "rotate-180" : ""), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), dropdownOpen && (_jsxs(Div, { className: "absolute left-0 top-full z-[var(--appkit-z-dropdown,50)] mt-1 min-w-[180px] max-w-[260px] rounded-xl border border-zinc-200 bg-white shadow-lg dark:border-slate-700 dark:bg-slate-900", children: [searchable && (_jsx(Div, { className: "border-b border-zinc-100 p-2 dark:border-slate-800", children: _jsx(Input, { type: "search", placeholder: "Search\u2026", value: search, onChange: (e) => handleSearch(e.target.value), className: "w-full rounded-md border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-900 focus:outline-none dark:border-slate-700 dark:bg-slate-800/60 dark:text-zinc-100" }) })), _jsxs(Div, { className: "max-h-56 overflow-y-auto py-1", children: [filtered.map((option) => {
|
|
65
|
+
const isSelected = selected.includes(option.value);
|
|
66
|
+
return (_jsxs(Row, { gap: "xs", className: "cursor-pointer rounded-lg px-3 py-1.5 hover:bg-zinc-50 dark:hover:bg-slate-800 mx-1", onClick: () => toggle(option.value), children: [_jsx("input", { type: selectionMode === "single" ? "radio" : "checkbox", checked: isSelected, onChange: () => toggle(option.value), "aria-label": option.label, className: "h-3.5 w-3.5 flex-shrink-0 cursor-pointer rounded border-zinc-300 text-primary-600 dark:border-slate-600 dark:text-secondary-500" }), _jsx(Span, { className: "flex-1 truncate text-xs text-zinc-700 dark:text-zinc-300", children: option.label }), option.count !== undefined && (_jsx(Span, { className: "flex-shrink-0 text-[10px] tabular-nums text-zinc-400 dark:text-zinc-500", children: option.count }))] }, option.value));
|
|
67
|
+
}), filtered.length === 0 && (_jsx(Text, { className: "px-3 py-2 text-xs text-zinc-400 dark:text-zinc-500", children: "No results" }))] }), hasValue && (_jsx(Div, { className: "border-t border-zinc-100 p-1 dark:border-slate-800", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "w-full py-1 text-xs text-zinc-400 transition-colors hover:text-red-600", onClick: () => { onChange([]); onClear?.(); setDropdownOpen(false); }, children: "Clear" }) }))] }))] }));
|
|
68
|
+
}
|
|
69
|
+
// ── Accordion variant (default) ─────────────────────────────────────────
|
|
47
70
|
return (_jsxs(Div, { className: cn("p-4 border-b border-zinc-200 dark:border-slate-700 last:border-b-0", className), children: [_jsxs(Row, { gap: "xs", children: [_jsxs(Button, { type: "button", onClick: handleToggle, variant: "ghost", size: "sm", className: "flex flex-1 items-center justify-between text-sm font-semibold text-zinc-900 dark:text-zinc-50 py-1 hover:opacity-80 transition-opacity", "aria-expanded": !isCollapsed, children: [_jsxs(Row, { as: Span, gap: "xs", children: [title, hasValue && (_jsx(Span, { className: "inline-flex items-center justify-center w-5 h-5 text-xs rounded-full bg-emerald-50 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 ring-1 ring-emerald-600/20 dark:ring-emerald-400/20", children: selected.length }))] }), _jsx("svg", { className: cn("w-4 h-4 text-zinc-500 dark:text-zinc-400 transition-transform duration-200", isCollapsed ? "" : "rotate-180"), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), onClear && hasValue && (_jsx(Button, { type: "button", onClick: onClear, variant: "ghost", size: "sm", className: "inline-flex items-center justify-center w-5 h-5 p-0 text-zinc-500 dark:text-zinc-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors rounded-full", "aria-label": "Clear filter", children: _jsx("svg", { className: "w-3 h-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }) }))] }), isCollapsed && hasValue && (_jsxs(Div, { className: "mt-2 flex flex-wrap gap-1", children: [selectedLabels.map((label) => (_jsx(Span, { className: "inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-zinc-100 dark:bg-slate-700 text-zinc-700 dark:text-zinc-300 border border-zinc-200 dark:border-slate-600", children: label }, label))), selected.length > 3 && (_jsxs(Span, { className: "inline-flex items-center px-2 py-0.5 rounded-full text-xs text-zinc-400 dark:text-zinc-500", children: ["+", selected.length - 3, " more"] }))] })), searchable && (_jsx(Div, { className: "mt-2", children: _jsx(Input, { type: "search", placeholder: "Search\u2026", value: search, onChange: (e) => handleSearch(e.target.value), className: "w-full rounded-md border border-zinc-200 dark:border-slate-700 bg-white dark:bg-slate-800/60 px-2.5 py-1.5 text-xs text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-primary-500/20 dark:focus:ring-secondary-400/20" }) })), !isCollapsed && (_jsxs(Div, { className: "mt-3 space-y-1", children: [filtered.map((option) => {
|
|
48
71
|
const isSelected = selected.includes(option.value);
|
|
49
72
|
return (_jsxs(Row, { gap: "2.5", className: "py-1 cursor-pointer group", children: [selectionMode === "single" ? (_jsx("input", { type: "radio", checked: isSelected, onChange: () => toggle(option.value), "aria-label": option.label, className: cn("flex-shrink-0 border-zinc-300 dark:border-slate-600 cursor-pointer", "w-4 h-4 rounded-full text-primary-600 dark:text-secondary-500", "focus:ring-primary-500/30 dark:focus:ring-secondary-400/30") })) : (_jsx("input", { type: "checkbox", checked: isSelected, onChange: () => toggle(option.value), "aria-label": option.label, className: cn("flex-shrink-0 border-zinc-300 dark:border-slate-600 cursor-pointer", "w-4 h-4 rounded text-primary-600 dark:text-secondary-500 checked:bg-primary-600 dark:checked:bg-secondary-500", "focus:ring-primary-500/30 dark:focus:ring-secondary-400/30") })), _jsx(Span, { className: "flex-1 text-sm text-zinc-700 dark:text-zinc-300 group-hover:text-zinc-900 dark:group-hover:text-zinc-100 transition-colors truncate", children: option.label }), option.count !== undefined && (_jsx(Span, { className: "text-xs text-zinc-400 dark:text-zinc-500 tabular-nums flex-shrink-0", children: option.count }))] }, option.value));
|
|
@@ -12,5 +12,8 @@ export interface FeaturedBundlesSectionProps {
|
|
|
12
12
|
description?: string;
|
|
13
13
|
initialItems?: CategoryDocument[];
|
|
14
14
|
className?: string;
|
|
15
|
+
onBuyNow?: (input: {
|
|
16
|
+
bundleSlug: string;
|
|
17
|
+
}) => Promise<unknown>;
|
|
15
18
|
}
|
|
16
|
-
export declare function FeaturedBundlesSection({ title, description, initialItems, className, }: FeaturedBundlesSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
19
|
+
export declare function FeaturedBundlesSection({ title, description, initialItems, className, onBuyNow, }: FeaturedBundlesSectionProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -4,19 +4,20 @@ import { Badge, Div, Heading, Row, Section, Stack, Text } from "../../../ui";
|
|
|
4
4
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
5
5
|
import { formatCurrency } from "../../../utils/number.formatter";
|
|
6
6
|
import { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "../../../_internal/shared/features/categories/bundle-copy";
|
|
7
|
+
import { BundleBuyNowCta } from "../../categories/components/BundleBuyNowCta";
|
|
7
8
|
const PLACEHOLDER_EMOJI = "📦";
|
|
8
|
-
export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitle, description, initialItems = [], className = "", }) {
|
|
9
|
+
export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitle, description, initialItems = [], className = "", onBuyNow, }) {
|
|
9
10
|
const items = initialItems.filter((c) => c.categoryType === "bundle");
|
|
10
|
-
return (_jsx(Section, { className: `py-10 ${className}`, children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", className: "flex-wrap", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-100", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES ?? "/bundles"), className: "text-sm font-medium text-[var(--appkit-color-primary)] hover:underline", children: BUNDLE_COPY.featured.viewAll })] }), items.length === 0 ? (_jsx(Div, { className: "rounded-xl border border-dashed border-zinc-200 py-12 text-center dark:border-zinc-700", children: _jsx(Text, { color: "muted", children: BUNDLE_COPY.featured.empty }) })) : (_jsx(Div, { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle }, bundle.id))) }))] }) }));
|
|
11
|
+
return (_jsx(Section, { className: `py-10 ${className}`, children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", className: "flex-wrap", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-100", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES ?? "/bundles"), className: "text-sm font-medium text-[var(--appkit-color-primary)] hover:underline", children: BUNDLE_COPY.featured.viewAll })] }), items.length === 0 ? (_jsx(Div, { className: "rounded-xl border border-dashed border-zinc-200 py-12 text-center dark:border-zinc-700", children: _jsx(Text, { color: "muted", children: BUNDLE_COPY.featured.empty }) })) : (_jsx(Div, { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle, onBuyNow: onBuyNow }, bundle.id))) }))] }) }));
|
|
11
12
|
}
|
|
12
|
-
function FeaturedBundleCard({ bundle }) {
|
|
13
|
+
function FeaturedBundleCard({ bundle, onBuyNow }) {
|
|
13
14
|
const stock = bundle.bundleStockStatus ?? "in_stock";
|
|
14
15
|
const memberCount = bundle.bundleProductIds?.length ?? 0;
|
|
15
16
|
const cover = bundle.display?.coverImage;
|
|
16
17
|
const href = String(ROUTES.PUBLIC.BUNDLE_DETAIL?.(bundle.slug) ?? "#");
|
|
17
|
-
return (_jsxs(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
return (_jsxs(Stack, { gap: "none", className: "rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900", children: [_jsxs(Link, { href: href, className: "group block p-3 hover:no-underline", children: [_jsx(Div, { className: "mb-2 aspect-square overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800", children: cover ? (
|
|
19
|
+
// eslint-disable-next-line @next/next/no-img-element, lir/no-raw-media-elements
|
|
20
|
+
_jsx("img", { src: cover, alt: bundle.name, className: "h-full w-full object-cover transition-transform group-hover:scale-105", loading: "lazy" })) : (_jsx(Div, { className: "flex h-full w-full items-center justify-center text-3xl", children: PLACEHOLDER_EMOJI })) }), _jsx(Text, { className: "line-clamp-2 text-sm font-semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1 flex-wrap", children: [_jsx(Text, { size: "sm", weight: "bold", children: bundle.bundlePriceInPaise
|
|
21
|
+
? formatCurrency(bundle.bundlePriceInPaise / 100, "INR")
|
|
22
|
+
: BUNDLE_COPY.featured.priceFallback }), _jsxs(Text, { size: "xs", color: "muted", children: ["\u00B7 ", BUNDLE_COPY.featured.itemCount(memberCount)] }), stock !== "in_stock" && (_jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stock], children: BUNDLE_COPY.stockBadge.listVariantOutOfStock }))] })] }), onBuyNow && (_jsx(Div, { className: "border-t border-zinc-100 p-3 pt-2 dark:border-zinc-800", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }));
|
|
22
23
|
}
|
|
@@ -11,7 +11,8 @@ import { orderRepository } from "../../orders/repository/orders.repository";
|
|
|
11
11
|
import { RefundStatusValues } from "../../orders/schemas";
|
|
12
12
|
import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
|
|
13
13
|
import { notificationRepository } from "../../admin/repository/notification.repository";
|
|
14
|
-
const
|
|
14
|
+
const DEFAULT_PLATFORM_FEE_PERCENT = 5;
|
|
15
|
+
const DEFAULT_GST_PERCENT = 18;
|
|
15
16
|
export async function issuePartialRefund(adminUid, orderId, deductFees, refundNote) {
|
|
16
17
|
const order = await orderRepository.findById(orderId);
|
|
17
18
|
if (!order)
|
|
@@ -21,11 +22,12 @@ export async function issuePartialRefund(adminUid, orderId, deductFees, refundNo
|
|
|
21
22
|
if (order.refundStatus === RefundStatusValues.COMPLETED)
|
|
22
23
|
throw new ValidationError("Order has already been fully refunded.");
|
|
23
24
|
const settings = await siteSettingsRepository.getSingleton();
|
|
24
|
-
const
|
|
25
|
-
|
|
25
|
+
const platformFeePercent = settings.commissions?.platformFeePercent ?? DEFAULT_PLATFORM_FEE_PERCENT;
|
|
26
|
+
const gstPercent = settings.commissions?.gstPercent ?? DEFAULT_GST_PERCENT;
|
|
27
|
+
const effectiveRate = platformFeePercent * (1 + gstPercent / 100);
|
|
26
28
|
const grossRefund = order.totalPrice;
|
|
27
29
|
const feeDeducted = deductFees
|
|
28
|
-
? parseFloat(((grossRefund *
|
|
30
|
+
? parseFloat(((grossRefund * effectiveRate) / 100).toFixed(2))
|
|
29
31
|
: 0;
|
|
30
32
|
const netRefund = parseFloat((grossRefund - feeDeducted).toFixed(2));
|
|
31
33
|
await orderRepository.update(orderId, {
|
|
@@ -73,10 +75,11 @@ export async function previewCancellationRefund(userId, orderId) {
|
|
|
73
75
|
if (order.paymentStatus !== "paid")
|
|
74
76
|
return null;
|
|
75
77
|
const settings = await siteSettingsRepository.getSingleton();
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
+
const platformFeePercent = settings.commissions?.platformFeePercent ?? DEFAULT_PLATFORM_FEE_PERCENT;
|
|
79
|
+
const gstPercent = settings.commissions?.gstPercent ?? DEFAULT_GST_PERCENT;
|
|
80
|
+
const effectiveRate = platformFeePercent * (1 + gstPercent / 100);
|
|
78
81
|
const grossRefund = order.totalPrice;
|
|
79
|
-
const feeDeducted = parseFloat(((grossRefund *
|
|
82
|
+
const feeDeducted = parseFloat(((grossRefund * effectiveRate) / 100).toFixed(2));
|
|
80
83
|
const netRefund = parseFloat((grossRefund - feeDeducted).toFixed(2));
|
|
81
84
|
return {
|
|
82
85
|
orderId,
|