@mohasinac/appkit 4.2.0 → 4.3.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/dist/_internal/client/features/layout/navActive.d.ts +17 -0
- package/dist/_internal/client/features/layout/navActive.js +33 -0
- package/dist/_internal/server/features/auth/actions.d.ts +0 -1
- package/dist/_internal/server/features/auth/actions.js +0 -1
- package/dist/_internal/server/features/bundles/data.d.ts +22 -4
- package/dist/_internal/server/features/bundles/data.js +41 -4
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/checkout/actions.js +7 -78
- package/dist/_internal/server/features/checkout/prize-bundle-gates.d.ts +5 -16
- package/dist/_internal/server/features/checkout/prize-bundle-gates.js +5 -30
- package/dist/_internal/server/features/orders/adapters.js +1 -14
- package/dist/_internal/server/features/products/actions.js +8 -3
- package/dist/_internal/server/features/products/service.d.ts +15 -0
- package/dist/_internal/server/features/products/service.js +41 -0
- package/dist/_internal/server/functions/firestore.d.ts +3 -1
- package/dist/_internal/server/functions/firestore.js +17 -1
- package/dist/_internal/server/functions/scheduled.d.ts +2 -5
- package/dist/_internal/server/functions/scheduled.js +6 -30
- package/dist/_internal/server/jobs/core/bundleStockSync.js +25 -7
- package/dist/_internal/server/jobs/core/countersReconcile.js +7 -1
- package/dist/_internal/server/jobs/core/index.d.ts +4 -4
- package/dist/_internal/server/jobs/core/index.js +4 -4
- package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.d.ts +19 -0
- package/dist/_internal/server/jobs/core/onPrizeDrawPaymentConfirmed.js +30 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.d.ts +2 -2
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +2 -2
- package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.d.ts +20 -0
- package/dist/_internal/server/jobs/core/prizeDrawAssignWinner.js +113 -0
- package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.d.ts +14 -0
- package/dist/_internal/server/jobs/core/prizeDrawExpiryReveal.js +74 -0
- package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.d.ts +22 -0
- package/dist/_internal/server/jobs/core/prizeDrawSoldOutReveal.js +68 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -4
- package/dist/_internal/server/jobs/handlers/index.js +8 -6
- package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.d.ts +3 -0
- package/dist/_internal/server/jobs/handlers/onPrizeDrawPaymentConfirmed.js +11 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawExpiryReveal.js +2 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.d.ts +3 -0
- package/dist/_internal/server/jobs/handlers/prizeDrawSoldOutReveal.js +4 -0
- package/dist/_internal/shared/actions/action-registry.js +40 -10
- package/dist/_internal/shared/checkout/rules/_defaults.js +1 -2
- package/dist/_internal/shared/checkout/rules/_limits.d.ts +3 -0
- package/dist/_internal/shared/checkout/rules/_limits.js +3 -0
- package/dist/_internal/shared/checkout/rules/_registry.d.ts +2 -1
- package/dist/_internal/shared/checkout/rules/_registry.js +2 -2
- package/dist/_internal/shared/checkout/rules/classified.rule.js +0 -1
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +20 -1
- package/dist/_internal/shared/checkout/rules/types.d.ts +17 -7
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +2 -0
- package/dist/_internal/shared/features/categories/bundle-copy.js +2 -0
- package/dist/_internal/shared/features/categories/bundle-pricing.d.ts +23 -0
- package/dist/_internal/shared/features/categories/bundle-pricing.js +25 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +1 -1
- package/dist/constants/api-endpoints.d.ts +6 -9
- package/dist/constants/api-endpoints.js +5 -5
- package/dist/constants/field-names.d.ts +17 -1
- package/dist/constants/field-names.js +17 -1
- package/dist/features/account/components/UserSidebar.js +21 -9
- package/dist/features/account/hooks/useNotifications.js +5 -0
- package/dist/features/admin/actions/admin-actions.js +6 -0
- package/dist/features/admin/actions/notification-actions.js +1 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +1 -1
- package/dist/features/admin/components/AdminGuideHubView.js +15 -1
- package/dist/features/admin/components/AdminPaymentsGuideView.d.ts +2 -0
- package/dist/features/admin/components/AdminPaymentsGuideView.js +36 -0
- package/dist/features/admin/components/AdminPrizeDrawsView.js +3 -3
- package/dist/features/admin/components/AdminSidebar.js +18 -9
- package/dist/features/admin/components/AdminSiteSettingsView.js +33 -6
- package/dist/features/admin/components/AdminTesterChecklistView.js +74 -22
- package/dist/features/admin/components/AdminWhatsAppGuideView.d.ts +2 -0
- package/dist/features/admin/components/AdminWhatsAppGuideView.js +35 -0
- package/dist/features/admin/hooks/useAdminListing.d.ts +4 -0
- package/dist/features/admin/hooks/useAdminListing.js +4 -2
- package/dist/features/admin/schemas/firestore.d.ts +13 -5
- package/dist/features/admin/schemas/firestore.js +1 -2
- package/dist/features/auctions/actions/bid-actions.js +3 -1
- package/dist/features/auctions/components/AuctionBidsTable.d.ts +6 -1
- package/dist/features/auctions/components/AuctionBidsTable.js +8 -4
- package/dist/features/auctions/components/AuctionDetailPageView.js +10 -13
- package/dist/features/auctions/components/CollapsibleBidHistory.d.ts +23 -2
- package/dist/features/auctions/components/CollapsibleBidHistory.js +22 -3
- package/dist/features/auctions/components/LiveBidPrice.d.ts +19 -0
- package/dist/features/auctions/components/LiveBidPrice.js +17 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +16 -1
- package/dist/features/auctions/hooks/useBids.d.ts +33 -0
- package/dist/features/auctions/hooks/useBids.js +32 -0
- package/dist/features/auctions/hooks/useLiveAuctionBid.d.ts +20 -0
- package/dist/features/auctions/hooks/useLiveAuctionBid.js +23 -0
- package/dist/features/auth/checkout-value-otp.d.ts +7 -10
- package/dist/features/auth/checkout-value-otp.js +14 -10
- package/dist/features/auth/server.d.ts +0 -1
- package/dist/features/auth/server.js +0 -1
- package/dist/features/blog/api/[slug]/route.d.ts +5 -0
- package/dist/features/blog/api/[slug]/route.js +12 -3
- package/dist/features/blog/components/BlogPostView.d.ts +2 -0
- package/dist/features/blog/components/BlogPostView.js +5 -4
- package/dist/features/blog/hooks/useBlog.d.ts +2 -0
- package/dist/features/blog/hooks/useBlog.js +2 -0
- package/dist/features/blog/repository/blog.repository.d.ts +7 -0
- package/dist/features/blog/repository/blog.repository.js +44 -0
- package/dist/features/blog/schemas/firestore.d.ts +3 -0
- package/dist/features/blog/schemas/firestore.js +1 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/index.d.ts +0 -2
- package/dist/features/cart/components/index.js +0 -1
- package/dist/features/cart/hooks/useCartCount.js +22 -5
- package/dist/features/cart/hooks/useGuestCart.js +11 -1
- package/dist/features/cart/utils/guest-cart.d.ts +4 -0
- package/dist/features/cart/utils/guest-cart.js +10 -0
- package/dist/features/cart/utils/pending-ops.d.ts +17 -0
- package/dist/features/cart/utils/pending-ops.js +46 -5
- package/dist/features/categories/components/BrandDetailPageView.js +6 -2
- package/dist/features/categories/components/BundleDetailView.js +4 -2
- package/dist/features/categories/components/CategoryDetailPageView.js +10 -2
- package/dist/features/categories/schemas/firestore.d.ts +7 -0
- package/dist/features/checkout/actions/index.d.ts +0 -1
- package/dist/features/checkout/actions/index.js +0 -1
- package/dist/features/checkout/schemas/firestore.d.ts +2 -2
- package/dist/features/events/actions/event-actions.d.ts +2 -0
- package/dist/features/events/actions/event-actions.js +5 -0
- package/dist/features/events/components/EventCard.js +2 -3
- package/dist/features/events/components/RelatedEventsCarousel.d.ts +15 -0
- package/dist/features/events/components/RelatedEventsCarousel.js +13 -0
- package/dist/features/events/components/index.d.ts +2 -0
- package/dist/features/events/components/index.js +1 -0
- package/dist/features/events/repository/events.repository.d.ts +6 -0
- package/dist/features/events/repository/events.repository.js +21 -0
- package/dist/features/history/repository/user-history.repository.d.ts +1 -1
- package/dist/features/history/utils/guest-history.d.ts +1 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +4 -2
- package/dist/features/homepage/components/FeaturedProductsSection.js +2 -2
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/layout/AppLayoutShell.js +44 -10
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +2 -2
- package/dist/features/media/MediaVideo.d.ts +7 -0
- package/dist/features/media/MediaVideo.js +36 -5
- package/dist/features/orders/components/OrdersList.js +1 -5
- package/dist/features/orders/schemas/firestore.d.ts +2 -8
- package/dist/features/orders/schemas/index.d.ts +3 -32
- package/dist/features/orders/schemas/index.js +1 -4
- package/dist/features/orders/types/index.d.ts +6 -9
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +2 -1
- package/dist/features/products/actions/product-actions.js +13 -10
- package/dist/features/products/components/MarketplaceBundleCard.d.ts +1 -1
- package/dist/features/products/components/MarketplaceBundleCard.js +7 -4
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawItemsEditor.js +2 -2
- package/dist/features/products/components/PrizeDrawWinnerMappingView.d.ts +14 -0
- package/dist/features/products/components/PrizeDrawWinnerMappingView.js +37 -0
- package/dist/features/products/components/PrizeRevealModal.d.ts +10 -23
- package/dist/features/products/components/PrizeRevealModal.js +18 -147
- package/dist/features/products/components/ProductDetailPageView.js +72 -30
- package/dist/features/products/components/ProductForm.js +7 -14
- package/dist/features/products/components/ProductGalleryClient.d.ts +10 -1
- package/dist/features/products/components/ProductGalleryClient.js +29 -10
- package/dist/features/products/components/ProductsIndexListing.js +2 -1
- package/dist/features/products/components/RelatedProductsCarousel.js +2 -2
- package/dist/features/products/components/ShowGroupSection.js +2 -2
- package/dist/features/products/components/SublistingCarouselSection.js +1 -1
- package/dist/features/products/components/index.d.ts +4 -2
- package/dist/features/products/components/index.js +1 -0
- package/dist/features/products/constants/action-defs.d.ts +1 -3
- package/dist/features/products/constants/action-defs.js +0 -5
- package/dist/features/products/repository/products.repository.d.ts +6 -0
- package/dist/features/products/repository/products.repository.js +20 -1
- package/dist/features/products/schemas/firestore.d.ts +28 -3
- package/dist/features/products/schemas/index.d.ts +11 -3
- package/dist/features/products/schemas/index.js +10 -2
- package/dist/features/products/types/index.d.ts +2 -1
- package/dist/features/reviews/actions/review-actions.js +1 -0
- package/dist/features/reviews/components/ReviewDetailPageView.js +8 -1
- package/dist/features/reviews/components/ReviewDetailShell.js +2 -1
- package/dist/features/reviews/components/ReviewsList.js +2 -1
- package/dist/features/reviews/schemas/firestore.d.ts +3 -0
- package/dist/features/reviews/schemas/index.d.ts +6 -0
- package/dist/features/reviews/schemas/index.js +2 -0
- package/dist/features/reviews/types/index.d.ts +3 -0
- package/dist/features/scams/actions/scam-actions.d.ts +3 -0
- package/dist/features/scams/actions/scam-actions.js +3 -2
- package/dist/features/scams/components/ScamProfileView.d.ts +2 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/repository/scammer.repository.d.ts +6 -0
- package/dist/features/scams/repository/scammer.repository.js +24 -0
- package/dist/features/seller/actions/seller-actions.js +8 -0
- package/dist/features/seller/components/SellerPrizeDrawsView.js +4 -0
- package/dist/features/seller/components/SellerSidebar.js +20 -9
- package/dist/features/stores/components/StoreDetailLayoutView.js +6 -6
- package/dist/features/stores/components/StoreGuideHubView.js +7 -1
- package/dist/features/stores/components/StoreNavTabs.d.ts +10 -1
- package/dist/features/stores/components/StoreNavTabs.js +22 -9
- package/dist/features/stores/components/StoreWhatsAppGuideView.d.ts +3 -0
- package/dist/features/stores/components/StoreWhatsAppGuideView.js +12 -0
- package/dist/features/stores/components/index.d.ts +2 -0
- package/dist/features/stores/components/index.js +1 -0
- package/dist/features/tester/actions/index.d.ts +1 -0
- package/dist/features/tester/actions/index.js +1 -0
- package/dist/features/tester/actions/leaderboard-actions.d.ts +3 -0
- package/dist/features/tester/actions/leaderboard-actions.js +5 -0
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +26 -4
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +13 -0
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +23 -0
- package/dist/features/tester/components/BugHunterLeaderboardView.js +18 -0
- package/dist/features/tester/components/index.d.ts +2 -0
- package/dist/features/tester/components/index.js +1 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +12 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +80 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.d.ts +4 -0
- package/dist/features/tester/repository/tester-checklist-response.repository.js +4 -0
- package/dist/features/tester/schemas/firestore.d.ts +22 -1
- package/dist/features/tester/schemas/firestore.js +8 -0
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +42 -7
- package/dist/features/tester/seed-data/events-tester-seed-data.js +23 -0
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +69 -31
- package/dist/features/tester/seed-data/products-tester-seed-data.js +154 -9
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +252 -15
- package/dist/features/wishlist/hooks/useGuestWishlist.js +11 -1
- package/dist/features/wishlist/hooks/useWishlistCount.js +16 -2
- package/dist/features/wishlist/utils/guest-wishlist.d.ts +5 -0
- package/dist/features/wishlist/utils/guest-wishlist.js +11 -0
- package/dist/index.d.ts +23 -22
- package/dist/index.js +31 -58
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +2 -2
- package/dist/next/routing/route-map.d.ts +9 -2
- package/dist/next/routing/route-map.js +4 -1
- package/dist/schemas/registry.d.ts +6 -26
- package/dist/seed/bids-seed-data.js +14 -2
- package/dist/seed/blog-posts-seed-data.js +2 -1
- package/dist/seed/categories-seed-data.js +11 -2
- package/dist/seed/events-seed-data.js +11 -11
- package/dist/seed/faq-seed-data.js +5 -1
- package/dist/seed/products-auctions-seed-data.js +2 -2
- package/dist/seed/products-preorders-seed-data.d.ts +2 -1
- package/dist/seed/products-prize-draws-seed-data.js +56 -10
- package/dist/seed/products-standard-seed-data.js +80 -3
- package/dist/seed/scammers-seed-data.js +75 -2
- package/dist/seed/site-settings-seed-data.js +5 -2
- package/dist/server-entry.d.ts +0 -1
- package/dist/server-entry.js +0 -1
- package/dist/server.d.ts +4 -20
- package/dist/server.js +10 -57
- package/dist/styles.css +1 -277
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/BaseListingCard.js +1 -1
- package/dist/ui/components/ImageLightbox.d.ts +9 -0
- package/dist/ui/components/ImageLightbox.js +6 -3
- package/dist/ui/components/ListingToolbar.d.ts +18 -1
- package/dist/ui/components/ListingToolbar.js +5 -2
- package/dist/ui/components/StickyToolbar.js +13 -8
- package/package.json +1 -1
|
@@ -37,6 +37,7 @@ export declare const reviewFirestoreSchema: z.ZodObject<{
|
|
|
37
37
|
id: z.ZodString;
|
|
38
38
|
productId: z.ZodString;
|
|
39
39
|
productTitle: z.ZodString;
|
|
40
|
+
listingType: z.ZodOptional<z.ZodString>;
|
|
40
41
|
storeId: z.ZodOptional<z.ZodString>;
|
|
41
42
|
storeName: z.ZodOptional<z.ZodString>;
|
|
42
43
|
userId: z.ZodString;
|
|
@@ -126,6 +127,7 @@ export declare const reviewFirestoreSchema: z.ZodObject<{
|
|
|
126
127
|
} | undefined;
|
|
127
128
|
storeId?: string | undefined;
|
|
128
129
|
featured?: boolean | undefined;
|
|
130
|
+
listingType?: string | undefined;
|
|
129
131
|
images?: string[] | undefined;
|
|
130
132
|
storeName?: string | undefined;
|
|
131
133
|
userAvatar?: string | undefined;
|
|
@@ -173,6 +175,7 @@ export declare const reviewFirestoreSchema: z.ZodObject<{
|
|
|
173
175
|
} | undefined;
|
|
174
176
|
storeId?: string | undefined;
|
|
175
177
|
featured?: boolean | undefined;
|
|
178
|
+
listingType?: string | undefined;
|
|
176
179
|
images?: string[] | undefined;
|
|
177
180
|
storeName?: string | undefined;
|
|
178
181
|
userAvatar?: string | undefined;
|
|
@@ -241,6 +244,7 @@ export declare const reviewSchema: z.ZodObject<{
|
|
|
241
244
|
id: z.ZodString;
|
|
242
245
|
productId: z.ZodString;
|
|
243
246
|
productTitle: z.ZodOptional<z.ZodString>;
|
|
247
|
+
listingType: z.ZodOptional<z.ZodString>;
|
|
244
248
|
storeSlug: z.ZodOptional<z.ZodString>;
|
|
245
249
|
storeName: z.ZodOptional<z.ZodString>;
|
|
246
250
|
userId: z.ZodString;
|
|
@@ -303,6 +307,7 @@ export declare const reviewSchema: z.ZodObject<{
|
|
|
303
307
|
createdAt?: string | undefined;
|
|
304
308
|
updatedAt?: string | undefined;
|
|
305
309
|
featured?: boolean | undefined;
|
|
310
|
+
listingType?: string | undefined;
|
|
306
311
|
images?: {
|
|
307
312
|
url: string;
|
|
308
313
|
thumbnailUrl?: string | undefined;
|
|
@@ -333,6 +338,7 @@ export declare const reviewSchema: z.ZodObject<{
|
|
|
333
338
|
createdAt?: string | undefined;
|
|
334
339
|
updatedAt?: string | undefined;
|
|
335
340
|
featured?: boolean | undefined;
|
|
341
|
+
listingType?: string | undefined;
|
|
336
342
|
images?: {
|
|
337
343
|
url: string;
|
|
338
344
|
thumbnailUrl?: string | undefined;
|
|
@@ -14,6 +14,7 @@ export const reviewFirestoreSchema = z.object({
|
|
|
14
14
|
id: z.string(),
|
|
15
15
|
productId: z.string(),
|
|
16
16
|
productTitle: z.string(),
|
|
17
|
+
listingType: z.string().optional(),
|
|
17
18
|
storeId: z.string().optional(),
|
|
18
19
|
storeName: z.string().optional(),
|
|
19
20
|
userId: z.string(),
|
|
@@ -72,6 +73,7 @@ export const reviewSchema = z.object({
|
|
|
72
73
|
id: z.string(),
|
|
73
74
|
productId: z.string(),
|
|
74
75
|
productTitle: z.string().optional(),
|
|
76
|
+
listingType: z.string().optional(),
|
|
75
77
|
storeSlug: z.string().optional(),
|
|
76
78
|
storeName: z.string().optional(),
|
|
77
79
|
userId: z.string(),
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ListingType } from "../../products/types/index";
|
|
1
2
|
export type ReviewStatus = "pending" | "approved" | "rejected";
|
|
2
3
|
export interface ReviewImage {
|
|
3
4
|
url: string;
|
|
@@ -14,6 +15,8 @@ export interface Review {
|
|
|
14
15
|
id: string;
|
|
15
16
|
productId: string;
|
|
16
17
|
productTitle?: string;
|
|
18
|
+
/** Denormalized from the product at review-creation time — drives the correct detail-page link. */
|
|
19
|
+
listingType?: ListingType;
|
|
17
20
|
storeSlug?: string;
|
|
18
21
|
storeName?: string;
|
|
19
22
|
userId: string;
|
|
@@ -3,7 +3,10 @@ export interface ScammerProfilePageData {
|
|
|
3
3
|
scammer: ScammerDocument;
|
|
4
4
|
incidents: ScammerIncidentDocument[];
|
|
5
5
|
comments: ScammerCommentDocument[];
|
|
6
|
+
/** Admin-curated cross-links — confirmed to be the same person under a different alias. */
|
|
6
7
|
relatedScammers: ScammerDocument[];
|
|
8
|
+
/** Other verified profiles with the same scamType — pattern similarity only, never identity. */
|
|
9
|
+
similarScamReports: ScammerDocument[];
|
|
7
10
|
}
|
|
8
11
|
export interface ScammerListResult {
|
|
9
12
|
items: ScammerDocument[];
|
|
@@ -57,12 +57,13 @@ export async function getScammerProfilePageData(id) {
|
|
|
57
57
|
const scammer = await getPublicScammerById(id);
|
|
58
58
|
if (!scammer)
|
|
59
59
|
return null;
|
|
60
|
-
const [incidents, comments, relatedScammers] = await Promise.all([
|
|
60
|
+
const [incidents, comments, relatedScammers, similarScamReports] = await Promise.all([
|
|
61
61
|
scammerRepository.listPublicIncidents(scammer.id),
|
|
62
62
|
scammerRepository.listPublicComments(scammer.id),
|
|
63
63
|
scammerRepository.findManyById(scammer.relatedScammerIds),
|
|
64
|
+
scammerRepository.findBySameType(scammer.scamType, scammer.id),
|
|
64
65
|
]);
|
|
65
|
-
return { scammer, incidents, comments, relatedScammers };
|
|
66
|
+
return { scammer, incidents, comments, relatedScammers, similarScamReports };
|
|
66
67
|
}
|
|
67
68
|
const TRUST_STATUS_CLEAR = { status: "clear", matchedProfileSlugs: [] };
|
|
68
69
|
/**
|
|
@@ -7,5 +7,6 @@ export interface ScamProfileViewProps {
|
|
|
7
7
|
incidents?: ScammerIncidentDocument[];
|
|
8
8
|
comments?: ScammerCommentDocument[];
|
|
9
9
|
relatedScammers?: ScammerDocument[];
|
|
10
|
+
similarScamReports?: ScammerDocument[];
|
|
10
11
|
}
|
|
11
|
-
export declare function ScamProfileView({ scammer, isAuthenticated, incidents, comments, relatedScammers, }: ScamProfileViewProps): React.JSX.Element;
|
|
12
|
+
export declare function ScamProfileView({ scammer, isAuthenticated, incidents, comments, relatedScammers, similarScamReports, }: ScamProfileViewProps): React.JSX.Element;
|
|
@@ -46,7 +46,7 @@ function ScammerCommentsSection({ comments, isAuthenticated, scammerId, }) {
|
|
|
46
46
|
function ScammerActionsColumn({ scammer, isAuthenticated, reportHref, contestLoginHref, }) {
|
|
47
47
|
return (_jsxs(Stack, { gap: "md", children: [_jsxs(Card, { variant: "elevated", padding: "md", children: [_jsx(CardHeader, { children: _jsx(Heading, { level: 3, className: "tracking-wide", size: "sm", weight: "semibold", transform: "uppercase", children: "Actions" }) }), _jsx(CardBody, { children: _jsxs(Stack, { gap: "sm", children: [isAuthenticated ? (_jsxs(Link, { href: reportHref, className: CLS_OUTLINE_BTN, children: [_jsx(AlertOctagon, { className: "h-4 w-4 text-[color:var(--appkit-color-danger,theme(colors.red.500))]" }), "Report another incident"] })) : (_jsxs(Link, { href: `${String(ROUTES.AUTH.LOGIN)}?redirect=${encodeURIComponent(reportHref)}`, className: CLS_OUTLINE_BTN, children: [_jsx(AlertOctagon, { className: "h-4 w-4 text-[color:var(--appkit-color-danger,theme(colors.red.500))]" }), "Sign in to report an incident"] })), isAuthenticated ? (_jsxs(Link, { href: `/scams/${scammer.id}/contest`, className: CLS_OUTLINE_BTN, children: [_jsx(Flag, { className: "h-4 w-4 text-[color:var(--appkit-color-warning,theme(colors.amber.500))]" }), "Contest this profile"] })) : (_jsxs(Link, { href: contestLoginHref, className: "appkit-button appkit-button--ghost appkit-button--md flex w-full items-center gap-[var(--appkit-space-2)]", children: [_jsx(Flag, { className: "h-4 w-4" }), "Sign in to contest"] })), _jsx(Text, { variant: "secondary", size: "xs", children: "All contest submissions are reviewed by our moderation team before any changes are made." })] }) })] }), _jsxs(Card, { variant: "outlined", padding: "md", children: [_jsx(CardHeader, { children: _jsx(Heading, { level: 3, className: "tracking-wide", size: "sm", weight: "semibold", transform: "uppercase", children: "Contest Options" }) }), _jsx(CardBody, { children: _jsx(Stack, { gap: "xs", as: "ul", children: Object.entries(CONTEST_TYPE_LABELS).map(([, label]) => (_jsxs(Row, { gap: "sm", align: "start", as: "li", children: [_jsx(Span, { className: "mt-1.5 h-1.5 w-1.5 shrink-0 bg-[color:var(--appkit-color-border,theme(colors.zinc.300))]", rounded: "full" }), _jsx(Text, { variant: "secondary", size: "xs", children: label })] }, label))) }) })] }), scammer.verifiedAt && (_jsx(Alert, { variant: "success", compact: true, children: _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Shield, { className: "h-3.5 w-3.5 shrink-0" }), _jsxs(Text, { size: "xs", weight: "medium", children: ["Verified by LetItRip moderation on ", formatDate(scammer.verifiedAt)] })] }) }))] }));
|
|
48
48
|
}
|
|
49
|
-
export function ScamProfileView({ scammer, isAuthenticated, incidents = [], comments = [], relatedScammers = [], }) {
|
|
49
|
+
export function ScamProfileView({ scammer, isAuthenticated, incidents = [], comments = [], relatedScammers = [], similarScamReports = [], }) {
|
|
50
50
|
const reportHref = String(ROUTES.PUBLIC.SCAM_REPORT);
|
|
51
51
|
const registryHref = String(ROUTES.PUBLIC.SCAMS);
|
|
52
52
|
const contestLoginHref = `${String(ROUTES.AUTH.LOGIN)}?redirect=${encodeURIComponent(`/scams/${scammer.id}`)}`;
|
|
@@ -55,5 +55,5 @@ export function ScamProfileView({ scammer, isAuthenticated, incidents = [], comm
|
|
|
55
55
|
{ label: "Home", href: "/" },
|
|
56
56
|
{ label: "Scam Registry", href: registryHref },
|
|
57
57
|
{ label: scammer.displayNames[0] ?? "Profile" },
|
|
58
|
-
] }) }) }), _jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Grid, { cols: "twoThird", gap: "lg", children: [_jsxs(Stack, { gap: "lg", children: [_jsx(ScammerHeaderCard, { scammer: scammer }), (scammer.phones.length > 0 || scammer.upiIds.length > 0 || scammer.emails.length > 0) && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Contact Identifiers" }), _jsxs(Grid, { cols: 2, gap: "sm", children: [scammer.phones.map((p) => (_jsx(IdentityChip, { label: "Phone", value: p, icon: _jsx(Phone, { className: "h-4 w-4" }) }, p))), scammer.upiIds.map((u) => (_jsx(IdentityChip, { label: "UPI ID", value: u, icon: _jsx(Wallet, { className: "h-4 w-4" }) }, u))), scammer.emails.map((e) => (_jsx(IdentityChip, { label: "Email", value: e, icon: _jsx(Mail, { className: "h-4 w-4" }) }, e)))] })] })), scammer.socialMedia.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Social Media Profiles" }), _jsx(Stack, { gap: "xs", children: scammer.socialMedia.map((sm, i) => (_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(ExternalLink, { className: "h-4 w-4 shrink-0 text-[color:var(--appkit-color-text-muted,theme(colors.zinc.400))]" }), _jsxs(Text, { size: "sm", weight: "medium", children: [SOCIAL_PLATFORM_LABELS[sm.platform] ?? sm.platform, ":"] }), sm.url ? (_jsx(Anchor, { href: sm.url, size: "sm", children: sm.handle })) : (_jsx(Text, { variant: "secondary", size: "sm", children: sm.handle }))] }, i))) })] })), scammer.itemInvolved && (_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Item Involved" }), _jsx(Text, { variant: "secondary", size: "sm", children: scammer.itemInvolved })] })), _jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "What Happened" }), _jsx(Card, { variant: "flat", padding: "md", children: _jsx(Text, { className: "leading-relaxed", size: "sm", children: scammer.description }) })] }), scamTypeDef && scamTypeDef.howToAvoid.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "How to Avoid This Scam" }), _jsx(Card, { variant: "flat", padding: "md", children: _jsx(Stack, { gap: "xs", as: "ol", children: scamTypeDef.howToAvoid.map((tip, i) => (_jsxs(Row, { gap: "sm", align: "start", as: "li", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "h-5 w-5 shrink-0 bg-[color:var(--appkit-color-success,theme(colors.green.600))]/10 text-[color:var(--appkit-color-success,theme(colors.green.700))]", rounded: "full", children: i + 1 }), _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: tip })] }, i))) }) })] })), scammer.evidence.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Evidence" }), _jsx(Row, { gap: "sm", wrap: true, children: scammer.evidence.map((url, i) => (_jsx(Anchor, { href: url, underline: "none", rounded: "lg", shadow: "sm", className: "block h-32 w-32 overflow-hidden border hover:opacity-90", children: _jsx(MediaImage, { src: url, alt: `Evidence ${i + 1}`, size: "thumbnail" }) }, i))) })] })), _jsx(ScammerIncidentsSection, { incidents: incidents }), _jsx(ScammerCommentsSection, { comments: comments, isAuthenticated: isAuthenticated, scammerId: scammer.id }), relatedScammers.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Related Profiles" }), _jsx(Stack, { gap: "sm", children: relatedScammers.map((rel) => (_jsx(Link, { href: String(ROUTES.PUBLIC.SCAM_DETAIL(rel.id)), className: "block", children: _jsx(Card, { variant: "outlined", padding: "sm", className: "hover:opacity-80 transition-opacity", children: _jsxs(Row, { gap: "sm", align: "center", justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "sm", weight: "medium", children: rel.displayNames[0] }), _jsx(Text, { variant: "secondary", size: "xs", children: SCAM_TYPE_LABELS[rel.scamType] ?? rel.scamType })] }), _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Badge, { variant: statusVariant(rel.status), children: SCAMMER_STATUS_LABELS[rel.status] ?? rel.status }), _jsx(Link2, { className: "h-4 w-4 text-[color:var(--appkit-color-text-muted,theme(colors.zinc.400))]" })] })] }) }) }, rel.id))) })] }))] }), _jsx(ScammerActionsColumn, { scammer: scammer, isAuthenticated: isAuthenticated, reportHref: reportHref, contestLoginHref: contestLoginHref })] }) }) })] }));
|
|
58
|
+
] }) }) }), _jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Grid, { cols: "twoThird", gap: "lg", children: [_jsxs(Stack, { gap: "lg", children: [_jsx(ScammerHeaderCard, { scammer: scammer }), (scammer.phones.length > 0 || scammer.upiIds.length > 0 || scammer.emails.length > 0) && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Contact Identifiers" }), _jsxs(Grid, { cols: 2, gap: "sm", children: [scammer.phones.map((p) => (_jsx(IdentityChip, { label: "Phone", value: p, icon: _jsx(Phone, { className: "h-4 w-4" }) }, p))), scammer.upiIds.map((u) => (_jsx(IdentityChip, { label: "UPI ID", value: u, icon: _jsx(Wallet, { className: "h-4 w-4" }) }, u))), scammer.emails.map((e) => (_jsx(IdentityChip, { label: "Email", value: e, icon: _jsx(Mail, { className: "h-4 w-4" }) }, e)))] })] })), scammer.socialMedia.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Social Media Profiles" }), _jsx(Stack, { gap: "xs", children: scammer.socialMedia.map((sm, i) => (_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(ExternalLink, { className: "h-4 w-4 shrink-0 text-[color:var(--appkit-color-text-muted,theme(colors.zinc.400))]" }), _jsxs(Text, { size: "sm", weight: "medium", children: [SOCIAL_PLATFORM_LABELS[sm.platform] ?? sm.platform, ":"] }), sm.url ? (_jsx(Anchor, { href: sm.url, size: "sm", children: sm.handle })) : (_jsx(Text, { variant: "secondary", size: "sm", children: sm.handle }))] }, i))) })] })), scammer.itemInvolved && (_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Item Involved" }), _jsx(Text, { variant: "secondary", size: "sm", children: scammer.itemInvolved })] })), _jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "What Happened" }), _jsx(Card, { variant: "flat", padding: "md", children: _jsx(Text, { className: "leading-relaxed", size: "sm", children: scammer.description }) })] }), scamTypeDef && scamTypeDef.howToAvoid.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "How to Avoid This Scam" }), _jsx(Card, { variant: "flat", padding: "md", children: _jsx(Stack, { gap: "xs", as: "ol", children: scamTypeDef.howToAvoid.map((tip, i) => (_jsxs(Row, { gap: "sm", align: "start", as: "li", children: [_jsx(Span, { layout: "flex-center", size: "xs", weight: "bold", className: "h-5 w-5 shrink-0 bg-[color:var(--appkit-color-success,theme(colors.green.600))]/10 text-[color:var(--appkit-color-success,theme(colors.green.700))]", rounded: "full", children: i + 1 }), _jsx(Text, { variant: "secondary", className: "leading-relaxed", size: "sm", children: tip })] }, i))) }) })] })), scammer.evidence.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Evidence" }), _jsx(Row, { gap: "sm", wrap: true, children: scammer.evidence.map((url, i) => (_jsx(Anchor, { href: url, underline: "none", rounded: "lg", shadow: "sm", className: "block h-32 w-32 overflow-hidden border hover:opacity-90", children: _jsx(MediaImage, { src: url, alt: `Evidence ${i + 1}`, size: "thumbnail" }) }, i))) })] })), _jsx(ScammerIncidentsSection, { incidents: incidents }), _jsx(ScammerCommentsSection, { comments: comments, isAuthenticated: isAuthenticated, scammerId: scammer.id }), relatedScammers.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Related Profiles" }), _jsx(Stack, { gap: "sm", children: relatedScammers.map((rel) => (_jsx(Link, { href: String(ROUTES.PUBLIC.SCAM_DETAIL(rel.id)), className: "block", children: _jsx(Card, { variant: "outlined", padding: "sm", className: "hover:opacity-80 transition-opacity", children: _jsxs(Row, { gap: "sm", align: "center", justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "sm", weight: "medium", children: rel.displayNames[0] }), _jsx(Text, { variant: "secondary", size: "xs", children: SCAM_TYPE_LABELS[rel.scamType] ?? rel.scamType })] }), _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Badge, { variant: statusVariant(rel.status), children: SCAMMER_STATUS_LABELS[rel.status] ?? rel.status }), _jsx(Link2, { className: "h-4 w-4 text-[color:var(--appkit-color-text-muted,theme(colors.zinc.400))]" })] })] }) }) }, rel.id))) })] })), similarScamReports.length > 0 && (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "base", weight: "semibold", children: "Similar Scam Reports" }), _jsxs(Text, { variant: "secondary", size: "xs", children: ["Other profiles reported for the same scam pattern (", SCAM_TYPE_LABELS[scammer.scamType] ?? scammer.scamType, "). This does not mean they are the same person."] }), _jsx(Stack, { gap: "sm", children: similarScamReports.map((rel) => (_jsx(Link, { href: String(ROUTES.PUBLIC.SCAM_DETAIL(rel.id)), className: "block", children: _jsx(Card, { variant: "outlined", padding: "sm", className: "hover:opacity-80 transition-opacity", children: _jsxs(Row, { gap: "sm", align: "center", justify: "between", children: [_jsxs(Stack, { gap: "none", children: [_jsx(Text, { size: "sm", weight: "medium", children: rel.displayNames[0] }), _jsx(Text, { variant: "secondary", size: "xs", children: SCAM_TYPE_LABELS[rel.scamType] ?? rel.scamType })] }), _jsxs(Row, { gap: "xs", align: "center", children: [_jsx(Badge, { variant: statusVariant(rel.status), children: SCAMMER_STATUS_LABELS[rel.status] ?? rel.status }), _jsx(Link2, { className: "h-4 w-4 text-[color:var(--appkit-color-text-muted,theme(colors.zinc.400))]" })] })] }) }) }, rel.id))) })] }))] }), _jsx(ScammerActionsColumn, { scammer: scammer, isAuthenticated: isAuthenticated, reportHref: reportHref, contestLoginHref: contestLoginHref })] }) }) })] }));
|
|
59
59
|
}
|
|
@@ -61,6 +61,12 @@ declare class ScammerRepository extends BaseRepository<ScammerDocument> {
|
|
|
61
61
|
findByContactField(field: "phones" | "upiIds" | "emails" | "displayNames", value: string): Promise<ScammerDocument[]>;
|
|
62
62
|
listPublicIncidents(scammerId: string): Promise<ScammerIncidentDocument[]>;
|
|
63
63
|
listPublicComments(scammerId: string): Promise<ScammerCommentDocument[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Other verified profiles with the same scamType — a "similar scam pattern"
|
|
66
|
+
* discovery signal, distinct from `relatedScammerIds` (which links profiles
|
|
67
|
+
* confirmed to be the same person). Never implies identity, only pattern similarity.
|
|
68
|
+
*/
|
|
69
|
+
findBySameType(scamType: string, excludeId: string, limit?: number): Promise<ScammerDocument[]>;
|
|
64
70
|
findManyById(ids: string[]): Promise<ScammerDocument[]>;
|
|
65
71
|
}
|
|
66
72
|
declare const scammerRepository: ScammerRepository;
|
|
@@ -142,6 +142,30 @@ class ScammerRepository extends BaseRepository {
|
|
|
142
142
|
return [];
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Other verified profiles with the same scamType — a "similar scam pattern"
|
|
147
|
+
* discovery signal, distinct from `relatedScammerIds` (which links profiles
|
|
148
|
+
* confirmed to be the same person). Never implies identity, only pattern similarity.
|
|
149
|
+
*/
|
|
150
|
+
async findBySameType(scamType, excludeId, limit = 5) {
|
|
151
|
+
try {
|
|
152
|
+
const snap = await this.getCollection()
|
|
153
|
+
.where(SCAMMER_FIELDS.STATUS, "==", "verified")
|
|
154
|
+
.where(SCAMMER_FIELDS.SCAM_TYPE, "==", scamType)
|
|
155
|
+
.orderBy(SCAMMER_FIELDS.CREATED_AT, "desc")
|
|
156
|
+
.limit(limit + 1)
|
|
157
|
+
.get();
|
|
158
|
+
return snap.docs
|
|
159
|
+
.map((d) => this.mapDoc(d))
|
|
160
|
+
.filter((s) => s.id !== excludeId)
|
|
161
|
+
.slice(0, limit);
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
void normalizeError(err);
|
|
165
|
+
serverLogger.warn("scammer-repo: findBySameType failed — returning empty", { scamType, error: err instanceof Error ? err.message : String(err) });
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
}
|
|
145
169
|
async findManyById(ids) {
|
|
146
170
|
if (!ids.length)
|
|
147
171
|
return [];
|
|
@@ -15,6 +15,7 @@ import { userRepository } from "../../auth/repository/user.repository";
|
|
|
15
15
|
import { storeRepository } from "../../stores/repository/store.repository";
|
|
16
16
|
import { productRepository } from "../../products/repository/products.repository";
|
|
17
17
|
import { ProductStatusValues } from "../../products/schemas";
|
|
18
|
+
import { assertPrizeDrawNotLocked, assertPrizeDrawWonItemsImmutable, } from "../../../_internal/server/features/products/service";
|
|
18
19
|
import { orderRepository } from "../../orders/repository/orders.repository";
|
|
19
20
|
import { OrderStatusValues } from "../../orders/schemas";
|
|
20
21
|
import { payoutRepository } from "../../payments/repository/payout.repository";
|
|
@@ -485,6 +486,12 @@ export async function sellerUpdateProduct(userId, userRole, productId, input) {
|
|
|
485
486
|
if (!store || existing.storeId !== store.id)
|
|
486
487
|
throw new AuthorizationError("You do not own this product");
|
|
487
488
|
}
|
|
489
|
+
if (typeof input.status === "string" &&
|
|
490
|
+
input.status !== "published" &&
|
|
491
|
+
existing.status === "published") {
|
|
492
|
+
assertPrizeDrawNotLocked(existing, "unpublished or archived");
|
|
493
|
+
}
|
|
494
|
+
assertPrizeDrawWonItemsImmutable(existing, input.prizeDrawItems);
|
|
488
495
|
const finalizedData = await finalizeProductMediaReferences(input);
|
|
489
496
|
const updated = await productRepository.updateProduct(productId, finalizedData);
|
|
490
497
|
serverLogger.info("sellerUpdateProduct", { userId, productId });
|
|
@@ -500,6 +507,7 @@ export async function sellerDeleteProduct(userId, userRole, productId) {
|
|
|
500
507
|
if (!store || existing.storeId !== store.id)
|
|
501
508
|
throw new AuthorizationError("You do not own this product");
|
|
502
509
|
}
|
|
510
|
+
assertPrizeDrawNotLocked(existing, "deleted");
|
|
503
511
|
await productRepository.delete(productId);
|
|
504
512
|
serverLogger.info("sellerDeleteProduct", { userId, productId });
|
|
505
513
|
}
|
|
@@ -132,6 +132,10 @@ export function SellerPrizeDrawsView({ children, onDelete, ...props }) {
|
|
|
132
132
|
label: ACTIONS.STORE["edit-listing"].label,
|
|
133
133
|
onClick: () => void dispatch({ type: "NAVIGATE", href: String(ROUTES.STORE.PRIZE_DRAWS_EDIT(row.id)) }),
|
|
134
134
|
},
|
|
135
|
+
{
|
|
136
|
+
label: "View Entries",
|
|
137
|
+
onClick: () => void dispatch({ type: "NAVIGATE", href: String(ROUTES.STORE.PRIZE_DRAW_ENTRIES(row.id)) }),
|
|
138
|
+
},
|
|
135
139
|
...(onDelete
|
|
136
140
|
? [
|
|
137
141
|
{
|
|
@@ -8,6 +8,7 @@ import { MediaImage } from "../../media/MediaImage";
|
|
|
8
8
|
import { BottomSheet } from "../../layout/BottomSheet";
|
|
9
9
|
import { SidebarCollapseToggle } from "../../../_internal/client/features/layout/SidebarCollapseToggle";
|
|
10
10
|
import { useSidebarSearch } from "../../../_internal/client/features/layout/useSidebarSearch";
|
|
11
|
+
import { findActiveNavGroup, findActiveNavItem } from "../../../_internal/client/features/layout/navActive";
|
|
11
12
|
const __O = {
|
|
12
13
|
hidden: "overflow-hidden",
|
|
13
14
|
yAuto: "overflow-y-auto",
|
|
@@ -18,26 +19,36 @@ const CLS_STORE_NAME = "text-[length:var(--appkit-text-sm)] font-semibold text-[
|
|
|
18
19
|
const CLS_NAV_ACTIVE = "bg-warning-surface dark:bg-warning-surface text-warning dark:text-warning";
|
|
19
20
|
const CLS_NAV_BADGE = "shrink-0 rounded-full bg-warning-surface px-[var(--appkit-space-1-5)] py-[var(--appkit-space-0-5)] text-[10px] text-white leading-none";
|
|
20
21
|
const CLS_NAV_ICON_ACTIVE = "text-warning dark:text-warning";
|
|
21
|
-
function isNavItemActive(item, activeHref) {
|
|
22
|
-
return activeHref === item.href;
|
|
23
|
-
}
|
|
24
22
|
function NavLink({ item, isActive, onClick }) {
|
|
25
23
|
return (_jsxs(Link, { href: item.href, onClick: onClick, className: `flex items-center justify-end gap-[var(--appkit-space-2-5)] rounded-lg px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[0.8125rem] font-medium leading-tight transition-colors ${isActive
|
|
26
24
|
? CLS_NAV_ACTIVE
|
|
27
25
|
: "text-[var(--appkit-color-text-muted)] hover:bg-zinc-50 hover:bg-[var(--appkit-color-surface-elevated)]/60 hover:text-zinc-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: [item.icon && _jsx(Span, { size: "base", className: "shrink-0 opacity-60", children: item.icon }), _jsx(Span, { className: "truncate", children: item.label }), item.badge != null && item.badge > 0 && (_jsx(Span, { weight: "bold", className: CLS_NAV_BADGE, children: item.badge }))] }));
|
|
28
26
|
}
|
|
29
27
|
function FlatContent({ items, activeHref, storeName, storeLogoURL, onItemClick, }) {
|
|
28
|
+
const activeItem = findActiveNavItem(items, activeHref);
|
|
30
29
|
return (_jsxs(_Fragment, { children: [storeName && (_jsxs(Row, { border: "bottom-subtle", gap: "3", padding: "inline", children: [storeLogoURL ? (_jsx(Div, { className: CLS_STORE_AVATAR, children: _jsx(MediaImage, { src: storeLogoURL, alt: storeName, size: "thumbnail" }) })) : (_jsx(Div, { className: CLS_STORE_FALLBACK, children: storeName[0]?.toUpperCase() })), _jsx(Text, { className: CLS_STORE_NAME, children: storeName })] })), _jsx(Nav, { "aria-label": "Store navigation", padding: "y-sm", children: _jsx(Ul, { paddingX: "x-sm", spacing: "2xs", children: items.map((item) => {
|
|
31
|
-
const isActive =
|
|
30
|
+
const isActive = activeItem?.href === item.href;
|
|
32
31
|
return (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: isActive, onClick: onItemClick }) }, item.href));
|
|
33
32
|
}) }) })] }));
|
|
34
33
|
}
|
|
35
34
|
function GroupsContent({ groups, activeHref, storeName, storeLogoURL, onItemClick, }) {
|
|
36
|
-
// Accordion — only one group open at a time.
|
|
35
|
+
// Accordion — only one group open at a time. Initial pick: the group
|
|
36
|
+
// containing the active path, else the first group with defaultOpen.
|
|
37
37
|
const [openGroup, setOpenGroup] = useState(() => {
|
|
38
|
-
const
|
|
39
|
-
|
|
38
|
+
const active = findActiveNavGroup(groups, activeHref);
|
|
39
|
+
if (active)
|
|
40
|
+
return active.title;
|
|
41
|
+
return groups.find((g) => g.defaultOpen)?.title ?? null;
|
|
40
42
|
});
|
|
43
|
+
// Re-sync whenever the route changes — the sidebar stays mounted across
|
|
44
|
+
// client-side navigation, so the lazy initializer above only fires once.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
const active = findActiveNavGroup(groups, activeHref);
|
|
47
|
+
if (active)
|
|
48
|
+
setOpenGroup(active.title);
|
|
49
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
50
|
+
}, [activeHref]);
|
|
51
|
+
const activeItem = findActiveNavItem(groups.flatMap((g) => g.items), activeHref);
|
|
41
52
|
const { query, setQuery, isSearching, filteredGroups } = useSidebarSearch(groups);
|
|
42
53
|
const toggle = useCallback((title) => {
|
|
43
54
|
if (isSearching)
|
|
@@ -46,10 +57,10 @@ function GroupsContent({ groups, activeHref, storeName, storeLogoURL, onItemClic
|
|
|
46
57
|
}, [isSearching]);
|
|
47
58
|
return (_jsxs(_Fragment, { children: [storeName && (_jsxs(Row, { border: "bottom-subtle", gap: "3", padding: "inline", children: [storeLogoURL ? (_jsx(Div, { className: CLS_STORE_AVATAR, children: _jsx(MediaImage, { src: storeLogoURL, alt: storeName, size: "thumbnail" }) })) : (_jsx(Div, { className: CLS_STORE_FALLBACK, children: storeName[0]?.toUpperCase() })), _jsx(Text, { className: CLS_STORE_NAME, children: storeName })] })), _jsxs(Nav, { "aria-label": "Store navigation", padding: "y-xs", children: [_jsx(Div, { padding: "x-md", paddingY: "y-sm", children: _jsx(Input, { value: query, onChange: (e) => setQuery(e.target.value), placeholder: "Search navigation\u2026", "aria-label": "Search navigation" }) }), isSearching && filteredGroups.length === 0 && (_jsx(Div, { padding: "x-md", paddingY: "y-sm", children: _jsxs(Span, { size: "xs", color: "muted", children: ["No matches for \u201C", query, "\u201D."] }) })), filteredGroups.map((group) => {
|
|
48
59
|
const isOpen = isSearching || openGroup === group.title;
|
|
49
|
-
const hasActive = group.items
|
|
60
|
+
const hasActive = !!findActiveNavItem(group.items, activeHref);
|
|
50
61
|
return (_jsxs(Div, { className: "mb-0.5", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: () => toggle(group.title), paddingX: "md", paddingY: "sm", weight: "semibold", rounded: "none", className: `w-full text-[0.6875rem] uppercase tracking-widest transition-colors ${hasActive && !isOpen
|
|
51
62
|
? CLS_NAV_ICON_ACTIVE
|
|
52
|
-
: "text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]"}`, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { children: group.title }), _jsx("svg", { className: `w-3 h-3 transition-transform duration-150 ${isOpen ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }) }), isOpen && (_jsx(Ul, { paddingX: "x-sm", paddingY: "y-bottom-xs", spacing: "2xs", children: group.items.map((item) => (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive:
|
|
63
|
+
: "text-[var(--appkit-color-text-faint)] hover:text-[var(--appkit-color-text-muted)]"}`, children: _jsxs(Row, { align: "center", justify: "between", className: "w-full", children: [_jsx(Span, { children: group.title }), _jsx("svg", { className: `w-3 h-3 transition-transform duration-150 ${isOpen ? "rotate-180" : ""}`, fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2.5, d: "M19 9l-7 7-7-7" }) })] }) }), isOpen && (_jsx(Ul, { paddingX: "x-sm", paddingY: "y-bottom-xs", spacing: "2xs", children: group.items.map((item) => (_jsx(Li, { children: _jsx(NavLink, { item: item, isActive: activeItem?.href === item.href, onClick: onItemClick }) }, item.href))) }))] }, group.title));
|
|
53
64
|
})] })] }));
|
|
54
65
|
}
|
|
55
66
|
function DrawerPanel({ title, onClose, children }) {
|
|
@@ -120,15 +120,15 @@ export async function StoreDetailLayoutView({ storeSlug, activeTab, children, sc
|
|
|
120
120
|
const lt = TAB_LISTING_TYPE[tab.id];
|
|
121
121
|
return lt ? isListingTypeEnabled(lt, settings) : true;
|
|
122
122
|
});
|
|
123
|
+
const dropdownTabs = visibleStoreTabs.map((tab) => ({
|
|
124
|
+
value: tab.id,
|
|
125
|
+
label: tabLabel(tab.label, listingCounts[tab.id]),
|
|
126
|
+
href: STORE_LISTING_HREF[tab.id](storeSlug),
|
|
127
|
+
}));
|
|
123
128
|
const tabs = [
|
|
124
|
-
...visibleStoreTabs.map((tab) => ({
|
|
125
|
-
value: tab.id,
|
|
126
|
-
label: tabLabel(tab.label, listingCounts[tab.id]),
|
|
127
|
-
href: STORE_LISTING_HREF[tab.id](storeSlug),
|
|
128
|
-
})),
|
|
129
129
|
{ value: "coupons", label: tabLabel("Coupons", couponsCount), href: String(ROUTES.PUBLIC.STORE_COUPONS(storeSlug)) },
|
|
130
130
|
{ value: "reviews", label: tabLabel("Reviews", reviewsCount), href: String(ROUTES.PUBLIC.STORE_REVIEWS(storeSlug)) },
|
|
131
131
|
{ value: "about", label: "About", href: String(ROUTES.PUBLIC.STORE_ABOUT(storeSlug)) },
|
|
132
132
|
];
|
|
133
|
-
return (_jsxs(Main, { children: [_jsx(StoreHeader, { store: store, trust: trust }), _jsxs(Container, { size: "xl", className: "mt-6", children: [_jsx(StoreNavTabs, { tabs: tabs, activeValue: activeTab }), _jsx(Section, { padding: "t-lg", children: children })] })] }));
|
|
133
|
+
return (_jsxs(Main, { children: [_jsx(StoreHeader, { store: store, trust: trust }), _jsxs(Container, { size: "xl", className: "mt-6", children: [_jsx(StoreNavTabs, { dropdownTabs: dropdownTabs, dropdownPlaceholder: "Browse listings", tabs: tabs, activeValue: activeTab }), _jsx(Section, { padding: "t-lg", children: children })] })] }));
|
|
134
134
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Package, ShoppingBag, Wallet, Settings, Shield, Headphones, CheckCircle2, Circle, BookOpen, } from "lucide-react";
|
|
2
|
+
import { Package, ShoppingBag, Wallet, Settings, Shield, Headphones, CheckCircle2, Circle, BookOpen, MessageSquare, } from "lucide-react";
|
|
3
3
|
import { Div, Heading, Progress, Row, Section, Stack, Text, TextLink } from "../../../ui";
|
|
4
4
|
import { ROUTES } from "../../../next/routing/route-map";
|
|
5
5
|
const CLS_CHECK_ICON = "w-5 h-5 flex-shrink-0 text-emerald-500";
|
|
@@ -36,6 +36,12 @@ const GUIDE_CARDS = [
|
|
|
36
36
|
description: "Admin-granted feature flags — auctions, pre-orders, lower commissions, and more.",
|
|
37
37
|
href: String(ROUTES.STORE.GUIDE_CAPABILITIES),
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
Icon: MessageSquare,
|
|
41
|
+
title: "WhatsApp Catalog Sync",
|
|
42
|
+
description: "Push your product catalog to Meta Commerce Manager for WhatsApp, Facebook, and Instagram Shops.",
|
|
43
|
+
href: String(ROUTES.STORE.GUIDE_WHATSAPP),
|
|
44
|
+
},
|
|
39
45
|
{
|
|
40
46
|
Icon: Headphones,
|
|
41
47
|
title: "Support",
|
|
@@ -6,10 +6,19 @@ export interface StoreTab {
|
|
|
6
6
|
}
|
|
7
7
|
export interface StoreNavTabsProps {
|
|
8
8
|
tabs: StoreTab[];
|
|
9
|
+
/**
|
|
10
|
+
* Listing-type tabs (Products/Auctions/Pre-Orders/.../Art & Stickers) —
|
|
11
|
+
* rendered as a single dropdown ahead of `tabs` instead of one row entry
|
|
12
|
+
* each, so the bar can't overflow no matter how many listing types are
|
|
13
|
+
* enabled. Picking an option navigates to its `href`.
|
|
14
|
+
*/
|
|
15
|
+
dropdownTabs?: StoreTab[];
|
|
16
|
+
/** Label shown in the dropdown when the active tab isn't a listing type (e.g. on Reviews/About). */
|
|
17
|
+
dropdownPlaceholder?: string;
|
|
9
18
|
activeValue?: string;
|
|
10
19
|
onTabChange?: (value: string) => void;
|
|
11
20
|
/** Render-prop for full custom tab bar */
|
|
12
21
|
renderTabBar?: (tabs: StoreTab[], activeValue: string | undefined, onChange: (v: string) => void) => React.ReactNode;
|
|
13
22
|
className?: string;
|
|
14
23
|
}
|
|
15
|
-
export declare function StoreNavTabs({ tabs, activeValue, onTabChange, renderTabBar, className, }: StoreNavTabsProps): React.JSX.Element;
|
|
24
|
+
export declare function StoreNavTabs({ tabs, dropdownTabs, dropdownPlaceholder, activeValue, onTabChange, renderTabBar, className, }: StoreNavTabsProps): React.JSX.Element;
|
|
@@ -1,13 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRouter } from "next/navigation";
|
|
4
|
+
import { Button, Div, Select, TextLink } from "../../../ui";
|
|
5
|
+
const CLS_ACTIVE_TAB = "border-primary text-primary";
|
|
6
|
+
export function StoreNavTabs({ tabs, dropdownTabs = [], dropdownPlaceholder = "Browse listings", activeValue, onTabChange, renderTabBar, className = "", }) {
|
|
7
|
+
const router = useRouter();
|
|
4
8
|
const handleChange = (v) => onTabChange?.(v);
|
|
5
9
|
if (renderTabBar) {
|
|
6
|
-
return _jsx(_Fragment, { children: renderTabBar(tabs, activeValue, handleChange) });
|
|
10
|
+
return _jsx(_Fragment, { children: renderTabBar([...dropdownTabs, ...tabs], activeValue, handleChange) });
|
|
7
11
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
const activeDropdownTab = dropdownTabs.find((tab) => tab.value === activeValue);
|
|
13
|
+
const handleDropdownChange = (value) => {
|
|
14
|
+
const tab = dropdownTabs.find((t) => t.value === value);
|
|
15
|
+
if (!tab)
|
|
16
|
+
return;
|
|
17
|
+
if (tab.href)
|
|
18
|
+
router.push(tab.href);
|
|
19
|
+
handleChange(tab.value);
|
|
20
|
+
};
|
|
21
|
+
return (_jsxs(Div, { layout: "flex", gap: "2", align: "center", role: "tablist", border: "bottom", className: `overflow-x-auto ${className}`, children: [dropdownTabs.length > 0 && (_jsx(Select, { bare: true, "aria-label": dropdownPlaceholder, options: dropdownTabs.map((tab) => ({ value: tab.value, label: tab.label })), value: activeDropdownTab?.value ?? "", placeholder: activeDropdownTab ? undefined : dropdownPlaceholder, onValueChange: handleDropdownChange, className: `max-w-[12rem] flex-shrink-0 whitespace-nowrap ${activeDropdownTab ? CLS_ACTIVE_TAB : ""}` })), tabs.map((tab) => tab.href ? (_jsx(TextLink, { variant: "bare", href: tab.href, role: "tab", "aria-selected": activeValue === tab.value, paddingX: "md", paddingY: "xs", size: "sm", weight: "medium", className: `whitespace-nowrap border-b-2 -mb-px transition-colors ${activeValue === tab.value
|
|
22
|
+
? CLS_ACTIVE_TAB
|
|
23
|
+
: "border-transparent text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:text-neutral-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: tab.label }, tab.value)) : (_jsx(Button, { variant: "ghost", type: "button", role: "tab", "aria-selected": activeValue === tab.value, onClick: () => handleChange(tab.value), paddingX: "md", paddingY: "sm", textSize: "sm", weight: "medium", rounded: "none", className: `whitespace-nowrap border-b-2 -mb-px transition-colors ${activeValue === tab.value
|
|
24
|
+
? CLS_ACTIVE_TAB
|
|
25
|
+
: "border-transparent text-[var(--appkit-color-text-muted)] text-[var(--appkit-color-text-muted)] hover:text-neutral-800 hover:text-[var(--appkit-color-text-muted)]"}`, children: tab.label }, tab.value)))] }));
|
|
13
26
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ArrowLeft } from "lucide-react";
|
|
3
|
+
import { Alert, Code, Div, Heading, Li, Section, Span, Stack, Text, TextLink, Ul } from "../../../ui";
|
|
4
|
+
import { ROUTES } from "../../../next/routing/route-map";
|
|
5
|
+
function GuideSection({ title, children, }) {
|
|
6
|
+
return (_jsxs(Section, { className: "border border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)]", rounded: "xl", shadow: "sm", padding: "lg", children: [_jsx(Heading, { level: 2, className: "mb-4 text-[var(--appkit-color-text)]", size: "lg", weight: "semibold", children: title }), children] }));
|
|
7
|
+
}
|
|
8
|
+
const LIST_DISC = "list-disc leading-relaxed";
|
|
9
|
+
const TEXT_MUTED = "leading-relaxed text-[var(--appkit-color-text-muted)]";
|
|
10
|
+
export function StoreWhatsAppGuideView(_props) {
|
|
11
|
+
return (_jsxs(Stack, { gap: "lg", padding: "b-2xl", children: [_jsxs(TextLink, { variant: "bare", href: String(ROUTES.STORE.GUIDE), size: "sm", layout: "inline-flex", align: "center", gap: "sm", className: "text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)] transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back to Seller Guide"] }), _jsxs(Div, { children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "2xl", weight: "bold", children: "WhatsApp Catalog Sync" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", size: "sm", children: "Push your product catalog to Meta's Commerce Manager for WhatsApp catalog messages and Facebook/Instagram Shops \u2014 and import products back the other way." })] }), _jsx(GuideSection, { title: "1. Create a Commerce Manager catalog", children: _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsxs(Li, { className: LIST_DISC, textSize: "sm", color: "muted", children: ["At ", _jsx(Span, { weight: "bold", children: "business.facebook.com/commerce" }), ", create a new catalog for the store (or use an existing one)."] }), _jsxs(Li, { className: LIST_DISC, textSize: "sm", color: "muted", children: ["Note the ", _jsx(Span, { weight: "bold", children: "Catalog ID" }), " shown in the catalog's settings."] })] }) }), _jsx(GuideSection, { title: "2. Generate a catalog-scoped access token", children: _jsxs(Text, { className: TEXT_MUTED, size: "sm", children: ["You need a Meta access token with ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "catalog_management" }), " permission, scoped to that catalog. This is generated the same way as the platform-level WhatsApp System User token (ask your admin \u2014 Admin Guide \u2192 WhatsApp Integration, step 3) but assigned to the ", _jsx(Span, { weight: "bold", children: "catalog" }), " asset instead of the WhatsApp Business Account asset."] }) }), _jsxs(GuideSection, { title: "3. Enter credentials in the seller dashboard", children: [_jsxs(Text, { className: "mb-2 leading-relaxed text-[var(--appkit-color-text-muted)]", size: "sm", children: [_jsx(Span, { weight: "bold", children: "Store \u2192 WhatsApp" }), " settings page:"] }), _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsx(Li, { className: LIST_DISC, textSize: "sm", color: "muted", children: "Catalog ID" }), _jsx(Li, { className: LIST_DISC, textSize: "sm", color: "muted", children: "Access Token" })] }), _jsxs(Text, { className: "mt-3 leading-relaxed text-[var(--appkit-color-text-muted)]", size: "sm", children: ["This is a store capability \u2014 it must be enabled for the store (", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "whatsapp_catalog_sync" }), " capability) before the settings page and sync actions are available."] })] }), _jsx(GuideSection, { title: "4. Push products to the catalog", children: _jsxs(Text, { className: TEXT_MUTED, size: "sm", children: ["Once connected, trigger a sync that batches published ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "standard" }), "-listing products (up to 50 per API call) to the Meta catalog via the Commerce API ", _jsx(Code, { size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "items_batch" }), " endpoint \u2014 title, description, price, image, availability, and condition."] }) }), _jsx(GuideSection, { title: "5. Import products from the catalog", children: _jsxs(Text, { className: TEXT_MUTED, size: "sm", children: ["The reverse flow fetches products already in the Meta catalog and creates them as local ", _jsx(Span, { weight: "bold", children: "draft" }), " listings, deduplicating against existing products by slug/retailer-id matching."] }) }), _jsxs(Alert, { variant: "info", children: ["This is unrelated to the platform-level WhatsApp order-notification credentials \u2014 those are for sending order-status messages to buyers, not catalog sync. Only published ", _jsx(Span, { weight: "bold", children: "standard" }), " listings sync \u2014 auctions, pre-orders, and other listing types are not included."] })] }));
|
|
12
|
+
}
|
|
@@ -47,3 +47,5 @@ export { StoreCapabilitiesGuideView } from "./StoreCapabilitiesGuideView";
|
|
|
47
47
|
export type { StoreCapabilitiesGuideViewProps } from "./StoreCapabilitiesGuideView";
|
|
48
48
|
export { StoreSettingsGuideView } from "./StoreSettingsGuideView";
|
|
49
49
|
export type { StoreSettingsGuideViewProps } from "./StoreSettingsGuideView";
|
|
50
|
+
export { StoreWhatsAppGuideView } from "./StoreWhatsAppGuideView";
|
|
51
|
+
export type { StoreWhatsAppGuideViewProps } from "./StoreWhatsAppGuideView";
|
|
@@ -20,3 +20,4 @@ export { StoreOrdersGuideView } from "./StoreOrdersGuideView";
|
|
|
20
20
|
export { StoreFinanceGuideView } from "./StoreFinanceGuideView";
|
|
21
21
|
export { StoreCapabilitiesGuideView } from "./StoreCapabilitiesGuideView";
|
|
22
22
|
export { StoreSettingsGuideView } from "./StoreSettingsGuideView";
|
|
23
|
+
export { StoreWhatsAppGuideView } from "./StoreWhatsAppGuideView";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { testerChecklistItemRepository } from "../repository/tester-checklist-item.repository";
|
|
2
|
+
/** Ranks testers by confirmed-bug count. Mirrors getEventLeaderboard()'s shape. */
|
|
3
|
+
export async function getBugHunterLeaderboard(limit = 50) {
|
|
4
|
+
return testerChecklistItemRepository.getBugHunterLeaderboard(limit);
|
|
5
|
+
}
|
|
@@ -1,18 +1,40 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useQuery } from "@tanstack/react-query";
|
|
4
|
-
import { Card, Details, Div, Stack, Summary, Text } from "../../../ui";
|
|
3
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { Button, Card, Details, Div, Row, Stack, Summary, Text } from "../../../ui";
|
|
5
5
|
import { MediaImage } from "../../media/MediaImage";
|
|
6
6
|
import { apiClient } from "../../../http";
|
|
7
|
+
import { useApiMutation } from "../../../client";
|
|
7
8
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
9
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
10
|
+
const REPORT_QUERY_KEY = ["admin", "tester-feedback", "report"];
|
|
8
11
|
export function AdminTesterFeedbackIssuesView() {
|
|
12
|
+
const queryClient = useQueryClient();
|
|
9
13
|
const query = useQuery({
|
|
10
|
-
queryKey:
|
|
14
|
+
queryKey: REPORT_QUERY_KEY,
|
|
11
15
|
queryFn: async () => {
|
|
12
16
|
const res = await apiClient.get(ADMIN_ENDPOINTS.TESTER_FEEDBACK_REPORT);
|
|
13
17
|
return res?.data ?? res;
|
|
14
18
|
},
|
|
15
19
|
});
|
|
20
|
+
const confirmBugMutation = useApiMutation({
|
|
21
|
+
mutationFn: async (responseId) => {
|
|
22
|
+
await apiClient.post(ADMIN_ENDPOINTS.TESTER_FEEDBACK_CONFIRM_BUG(responseId), {});
|
|
23
|
+
},
|
|
24
|
+
successMessage: "Bug confirmed and credited.",
|
|
25
|
+
onSuccess: () => {
|
|
26
|
+
queryClient.invalidateQueries({ queryKey: REPORT_QUERY_KEY });
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const reopenMutation = useApiMutation({
|
|
30
|
+
mutationFn: async (checklistItemId) => {
|
|
31
|
+
await apiClient.post(ADMIN_ENDPOINTS.TESTER_CHECKLIST_ITEM_REOPEN(checklistItemId), {});
|
|
32
|
+
},
|
|
33
|
+
successMessage: "Case reopened for retest.",
|
|
34
|
+
onSuccess: () => {
|
|
35
|
+
queryClient.invalidateQueries({ queryKey: REPORT_QUERY_KEY });
|
|
36
|
+
},
|
|
37
|
+
});
|
|
16
38
|
const issues = query.data?.issues ?? [];
|
|
17
39
|
if (query.isLoading)
|
|
18
40
|
return _jsx(Text, { color: "muted", children: "Loading issues\u2026" });
|
|
@@ -28,6 +50,6 @@ export function AdminTesterFeedbackIssuesView() {
|
|
|
28
50
|
const sortedPhases = Array.from(byPhase.keys()).sort((a, b) => a - b);
|
|
29
51
|
return (_jsx(Stack, { gap: "md", children: sortedPhases.map((phase) => {
|
|
30
52
|
const phaseIssues = byPhase.get(phase);
|
|
31
|
-
return (_jsxs(Details, { tone: "card", defaultOpen: sortedPhases.length === 1, children: [_jsxs(Summary, { size: "lg", weight: "bold", children: ["Phase ", phase, " (", phaseIssues.length, " issue", phaseIssues.length === 1 ? "" : "s", ")"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: phaseIssues.map((issue) => (_jsx(Card, { padding: "md", variant: "default", children: _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "semibold", children: issue.label }), _jsxs(Text, { size: "sm", color: "muted", children: [issue.groupLabel, " \u203A ", issue.pageLabel] }), _jsxs(Text, { size: "sm", color: "muted", children: ["Reported by ", issue.testerDisplayName] }), issue.comment && _jsx(Text, { size: "sm", children: issue.comment }), issue.screenshotUrl && (_jsx(MediaImage, { src: issue.screenshotUrl, alt: "Tester screenshot", size: "card" }))] }) }, issue.id))) }) })] }, phase));
|
|
53
|
+
return (_jsxs(Details, { tone: "card", defaultOpen: sortedPhases.length === 1, children: [_jsxs(Summary, { size: "lg", weight: "bold", children: ["Phase ", phase, " (", phaseIssues.length, " issue", phaseIssues.length === 1 ? "" : "s", ")"] }), _jsx(Div, { padding: "t-sm", children: _jsx(Stack, { gap: "sm", children: phaseIssues.map((issue) => (_jsx(Card, { padding: "md", variant: "default", children: _jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "semibold", children: issue.label }), _jsxs(Text, { size: "sm", color: "muted", children: [issue.groupLabel, " \u203A ", issue.pageLabel] }), _jsxs(Text, { size: "sm", color: "muted", children: ["Reported by ", issue.testerDisplayName] }), issue.comment && _jsx(Text, { size: "sm", children: issue.comment }), issue.screenshotUrl && (_jsx(MediaImage, { src: issue.screenshotUrl, alt: "Tester screenshot", size: "card" })), issue.bugConfirmed ? (_jsxs(Row, { gap: "sm", align: "center", wrap: true, children: [_jsxs(Text, { size: "sm", weight: "semibold", color: "success", children: ["\uD83D\uDC1B Confirmed \u2014 credited to ", issue.bugHunterName ?? "unknown tester"] }), !issue.supersededByItemId && (_jsx(Button, { size: "sm", action: ACTIONS.ADMIN["reopen-checklist-item"], isLoading: reopenMutation.isPending, onClick: () => reopenMutation.mutate(issue.checklistItemId) }))] })) : (_jsx(Div, { children: _jsx(Button, { size: "sm", action: ACTIONS.ADMIN["confirm-bug"], isLoading: confirmBugMutation.isPending, onClick: () => confirmBugMutation.mutate(issue.id) }) }))] }) }, issue.id))) }) })] }, phase));
|
|
32
54
|
}) }));
|
|
33
55
|
}
|
|
@@ -47,6 +47,14 @@ export function AdminTesterFeedbackListView(_props) {
|
|
|
47
47
|
queryClient.invalidateQueries({ queryKey: ["admin", "tester-feedback"] });
|
|
48
48
|
},
|
|
49
49
|
});
|
|
50
|
+
const confirmBugMutation = useApiMutation({
|
|
51
|
+
mutationFn: async (id) => {
|
|
52
|
+
await apiClient.post(ADMIN_ENDPOINTS.TESTER_FEEDBACK_CONFIRM_BUG(id), {});
|
|
53
|
+
},
|
|
54
|
+
onSuccess: () => {
|
|
55
|
+
queryClient.invalidateQueries({ queryKey: ["admin", "tester-feedback"] });
|
|
56
|
+
},
|
|
57
|
+
});
|
|
50
58
|
const config = {
|
|
51
59
|
portal: "admin",
|
|
52
60
|
title: "All Submissions",
|
|
@@ -95,6 +103,11 @@ export function AdminTesterFeedbackListView(_props) {
|
|
|
95
103
|
disabled: row.status === "reviewed",
|
|
96
104
|
onClick: () => patchMutation.mutate(row.id),
|
|
97
105
|
},
|
|
106
|
+
{
|
|
107
|
+
label: ACTIONS.ADMIN["confirm-bug"].label,
|
|
108
|
+
disabled: row.answer !== "no",
|
|
109
|
+
onClick: () => confirmBugMutation.mutate(row.id),
|
|
110
|
+
},
|
|
98
111
|
] })),
|
|
99
112
|
};
|
|
100
113
|
return _jsx(DataListingView, { config: config });
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { BugHunterLeaderboardEntry } from "../schemas/firestore";
|
|
3
|
+
export interface BugHunterLeaderboardViewProps {
|
|
4
|
+
isLoading?: boolean;
|
|
5
|
+
isEmpty?: boolean;
|
|
6
|
+
/** Render the full ranked list — caller provides entries via hook */
|
|
7
|
+
renderList?: () => React.ReactNode;
|
|
8
|
+
/** Render a single row; used when caller wants item-level control */
|
|
9
|
+
renderEntry?: (entry: BugHunterLeaderboardEntry, index: number) => React.ReactNode;
|
|
10
|
+
/** Entries — used only when renderEntry is provided */
|
|
11
|
+
entries?: BugHunterLeaderboardEntry[];
|
|
12
|
+
/** Empty state node */
|
|
13
|
+
renderEmpty?: () => React.ReactNode;
|
|
14
|
+
/** Loading spinner / skeleton */
|
|
15
|
+
renderSkeleton?: () => React.ReactNode;
|
|
16
|
+
labels?: {
|
|
17
|
+
title?: string;
|
|
18
|
+
noEntries?: string;
|
|
19
|
+
bugs?: string;
|
|
20
|
+
};
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function BugHunterLeaderboardView({ isLoading, isEmpty, renderList, renderEntry, entries, renderEmpty, renderSkeleton, labels, className, }: BugHunterLeaderboardViewProps): React.JSX.Element;
|