@mohasinac/appkit 4.2.0 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_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/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/AdminPrizeDrawsView.js +3 -3
- package/dist/features/admin/components/AdminSiteSettingsView.js +33 -6
- package/dist/features/admin/components/AdminTesterChecklistView.js +74 -22
- 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 +21 -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 +1 -2
- 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 +1 -1
- 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 +0 -1
- package/dist/features/products/constants/action-defs.js +0 -2
- 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 +26 -2
- 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/stores/components/StoreDetailLayoutView.js +6 -6
- package/dist/features/stores/components/StoreNavTabs.d.ts +10 -1
- package/dist/features/stores/components/StoreNavTabs.js +21 -9
- 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 +152 -8
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +249 -14
- 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 +19 -22
- package/dist/index.js +25 -58
- package/dist/jobs.d.ts +1 -1
- package/dist/jobs.js +2 -2
- package/dist/next/routing/route-map.d.ts +2 -0
- package/dist/next/routing/route-map.js +1 -0
- 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 +82 -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 -1
- 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
|
@@ -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
|
{
|
|
@@ -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
|
}
|
|
@@ -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,25 @@
|
|
|
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
|
+
export function StoreNavTabs({ tabs, dropdownTabs = [], dropdownPlaceholder = "Browse listings", activeValue, onTabChange, renderTabBar, className = "", }) {
|
|
6
|
+
const router = useRouter();
|
|
4
7
|
const handleChange = (v) => onTabChange?.(v);
|
|
5
8
|
if (renderTabBar) {
|
|
6
|
-
return _jsx(_Fragment, { children: renderTabBar(tabs, activeValue, handleChange) });
|
|
9
|
+
return _jsx(_Fragment, { children: renderTabBar([...dropdownTabs, ...tabs], activeValue, handleChange) });
|
|
7
10
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
const activeDropdownTab = dropdownTabs.find((tab) => tab.value === activeValue);
|
|
12
|
+
const handleDropdownChange = (value) => {
|
|
13
|
+
const tab = dropdownTabs.find((t) => t.value === value);
|
|
14
|
+
if (!tab)
|
|
15
|
+
return;
|
|
16
|
+
if (tab.href)
|
|
17
|
+
router.push(tab.href);
|
|
18
|
+
handleChange(tab.value);
|
|
19
|
+
};
|
|
20
|
+
return (_jsxs(Div, { layout: "flex", gap: "2", role: "tablist", border: "bottom", className: `items-center 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 ? "border-primary text-primary" : ""}` })), 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
|
|
21
|
+
? "border-primary text-primary"
|
|
22
|
+
: "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
|
|
23
|
+
? "border-primary text-primary"
|
|
24
|
+
: "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
25
|
}
|
|
@@ -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;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { Div, Row, Span, Stack, Text } from "../../../ui";
|
|
4
|
+
export function BugHunterLeaderboardView({ isLoading = false, isEmpty = false, renderList, renderEntry, entries = [], renderEmpty, renderSkeleton, labels = {}, className = "", }) {
|
|
5
|
+
if (isLoading) {
|
|
6
|
+
if (renderSkeleton)
|
|
7
|
+
return _jsx(_Fragment, { children: renderSkeleton() });
|
|
8
|
+
return _jsx(Row, { justify: "center", padding: "y-xl", children: "Loading\u2026" });
|
|
9
|
+
}
|
|
10
|
+
if (isEmpty) {
|
|
11
|
+
if (renderEmpty)
|
|
12
|
+
return _jsx(_Fragment, { children: renderEmpty() });
|
|
13
|
+
return (_jsx(Text, { paddingY: "md", variant: "secondary", size: "sm", align: "start", children: labels.noEntries ?? "No confirmed bugs yet." }));
|
|
14
|
+
}
|
|
15
|
+
if (renderList)
|
|
16
|
+
return _jsx(Div, { className: className, children: renderList() });
|
|
17
|
+
return (_jsx(Stack, { className: `${className}`, gap: "sm", children: entries.map((entry, i) => renderEntry ? (_jsx(React.Fragment, { children: renderEntry(entry, i) }, entry.hunterId)) : (_jsxs(Div, { layout: "flex", align: "center", justify: "between", rounded: "xl", padding: "sm", className: "border", "data-section": "bughunterleaderboard-div", children: [_jsxs(Span, { weight: "medium", children: ["#", entry.rank, " ", entry.hunterName] }), _jsxs(Span, { children: [entry.bugCount, " ", labels.bugs ?? "bugs"] })] }, entry.hunterId))) }));
|
|
18
|
+
}
|
|
@@ -9,3 +9,5 @@ export { AdminTesterFeedbackReportView } from "./AdminTesterFeedbackReportView";
|
|
|
9
9
|
export { AdminTesterFeedbackIssuesView } from "./AdminTesterFeedbackIssuesView";
|
|
10
10
|
export { AdminTesterFeedbackListView } from "./AdminTesterFeedbackListView";
|
|
11
11
|
export type { AdminTesterFeedbackListViewProps } from "./AdminTesterFeedbackListView";
|
|
12
|
+
export { BugHunterLeaderboardView } from "./BugHunterLeaderboardView";
|
|
13
|
+
export type { BugHunterLeaderboardViewProps } from "./BugHunterLeaderboardView";
|
|
@@ -5,3 +5,4 @@ export { AdminTesterFeedbackView } from "./AdminTesterFeedbackView";
|
|
|
5
5
|
export { AdminTesterFeedbackReportView } from "./AdminTesterFeedbackReportView";
|
|
6
6
|
export { AdminTesterFeedbackIssuesView } from "./AdminTesterFeedbackIssuesView";
|
|
7
7
|
export { AdminTesterFeedbackListView } from "./AdminTesterFeedbackListView";
|
|
8
|
+
export { BugHunterLeaderboardView } from "./BugHunterLeaderboardView";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { BaseRepository } from "../../../providers/db-firebase";
|
|
2
2
|
import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
|
|
3
|
-
import { type TesterChecklistItemDocument, type TesterChecklistItemCreateInput, type TesterChecklistItemUpdateInput } from "../schemas/firestore";
|
|
3
|
+
import { type TesterChecklistItemDocument, type TesterChecklistItemCreateInput, type TesterChecklistItemUpdateInput, type BugHunterLeaderboardEntry } from "../schemas/firestore";
|
|
4
4
|
export declare class TesterChecklistItemRepository extends BaseRepository<TesterChecklistItemDocument> {
|
|
5
5
|
static readonly SIEVE_FIELDS: FirebaseSieveFields;
|
|
6
6
|
constructor();
|
|
@@ -9,5 +9,16 @@ export declare class TesterChecklistItemRepository extends BaseRepository<Tester
|
|
|
9
9
|
update(id: string, data: TesterChecklistItemUpdateInput): Promise<TesterChecklistItemDocument>;
|
|
10
10
|
listActive(): Promise<TesterChecklistItemDocument[]>;
|
|
11
11
|
list(model: SieveModel): Promise<FirebaseSieveResult<TesterChecklistItemDocument>>;
|
|
12
|
+
/** Confirms a reported "No" as a real bug: credits the reporting tester and
|
|
13
|
+
* disables the case for all other testers. Credit is permanent — never
|
|
14
|
+
* touched again by reopenAsNewVersion(). */
|
|
15
|
+
confirmBug(id: string, hunterId: string, hunterName: string): Promise<TesterChecklistItemDocument>;
|
|
16
|
+
/** Reopens a fixed, bug-confirmed case as a new, active version for retest.
|
|
17
|
+
* The old item stays disabled forever with its bug-hunter credit intact. */
|
|
18
|
+
reopenAsNewVersion(oldItemId: string): Promise<TesterChecklistItemDocument>;
|
|
19
|
+
/** Single-query, in-memory aggregation of bug credits per hunter — mirrors
|
|
20
|
+
* EventEntryRepository.getLeaderboard()'s shape. Includes old/disabled/
|
|
21
|
+
* superseded items, since bug credit is permanent. */
|
|
22
|
+
getBugHunterLeaderboard(limit?: number): Promise<BugHunterLeaderboardEntry[]>;
|
|
12
23
|
}
|
|
13
24
|
export declare const testerChecklistItemRepository: TesterChecklistItemRepository;
|
|
@@ -42,6 +42,85 @@ export class TesterChecklistItemRepository extends BaseRepository {
|
|
|
42
42
|
async list(model) {
|
|
43
43
|
return this.sieveQuery(model, TesterChecklistItemRepository.SIEVE_FIELDS);
|
|
44
44
|
}
|
|
45
|
+
/** Confirms a reported "No" as a real bug: credits the reporting tester and
|
|
46
|
+
* disables the case for all other testers. Credit is permanent — never
|
|
47
|
+
* touched again by reopenAsNewVersion(). */
|
|
48
|
+
async confirmBug(id, hunterId, hunterName) {
|
|
49
|
+
const current = await this.findById(id);
|
|
50
|
+
if (!current) {
|
|
51
|
+
throw new DatabaseError(`Failed to confirm bug: missing checklist item ${id}`);
|
|
52
|
+
}
|
|
53
|
+
if (current.bugConfirmed) {
|
|
54
|
+
throw new DatabaseError(`Checklist item ${id} already has a confirmed bug`);
|
|
55
|
+
}
|
|
56
|
+
return this.update(id, {
|
|
57
|
+
isActive: false,
|
|
58
|
+
bugConfirmed: true,
|
|
59
|
+
bugHunterId: hunterId,
|
|
60
|
+
bugHunterName: hunterName,
|
|
61
|
+
bugConfirmedAt: new Date(),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** Reopens a fixed, bug-confirmed case as a new, active version for retest.
|
|
65
|
+
* The old item stays disabled forever with its bug-hunter credit intact. */
|
|
66
|
+
async reopenAsNewVersion(oldItemId) {
|
|
67
|
+
const old = await this.findById(oldItemId);
|
|
68
|
+
if (!old) {
|
|
69
|
+
throw new DatabaseError(`Failed to reopen checklist item: missing document ${oldItemId}`);
|
|
70
|
+
}
|
|
71
|
+
if (!old.bugConfirmed) {
|
|
72
|
+
throw new DatabaseError(`Checklist item ${oldItemId} has no confirmed bug to reopen`);
|
|
73
|
+
}
|
|
74
|
+
if (old.supersededByItemId) {
|
|
75
|
+
throw new DatabaseError(`Checklist item ${oldItemId} has already been reopened`);
|
|
76
|
+
}
|
|
77
|
+
const nextVersion = (old.version ?? 1) + 1;
|
|
78
|
+
const newId = `${old.id}-v${nextVersion}`;
|
|
79
|
+
const newItem = await this.createWithId(newId, {
|
|
80
|
+
groupKey: old.groupKey,
|
|
81
|
+
groupLabel: old.groupLabel,
|
|
82
|
+
pageKey: old.pageKey,
|
|
83
|
+
pageLabel: old.pageLabel,
|
|
84
|
+
label: old.label,
|
|
85
|
+
description: old.description,
|
|
86
|
+
href: old.href,
|
|
87
|
+
order: old.order,
|
|
88
|
+
phase: old.phase,
|
|
89
|
+
adminOnly: old.adminOnly,
|
|
90
|
+
isActive: true,
|
|
91
|
+
version: nextVersion,
|
|
92
|
+
previousVersionId: old.id,
|
|
93
|
+
searchTokens: this.buildSearchTokens(old),
|
|
94
|
+
});
|
|
95
|
+
await this.update(old.id, { supersededByItemId: newId });
|
|
96
|
+
return newItem;
|
|
97
|
+
}
|
|
98
|
+
/** Single-query, in-memory aggregation of bug credits per hunter — mirrors
|
|
99
|
+
* EventEntryRepository.getLeaderboard()'s shape. Includes old/disabled/
|
|
100
|
+
* superseded items, since bug credit is permanent. */
|
|
101
|
+
async getBugHunterLeaderboard(limit = 50) {
|
|
102
|
+
const snapshot = await this.db
|
|
103
|
+
.collection(this.collection)
|
|
104
|
+
.where(TESTER_CHECKLIST_ITEM_FIELDS.BUG_CONFIRMED, "==", true)
|
|
105
|
+
.get();
|
|
106
|
+
const byHunter = new Map();
|
|
107
|
+
for (const doc of snapshot.docs) {
|
|
108
|
+
const item = this.mapDoc(doc);
|
|
109
|
+
if (!item.bugHunterId)
|
|
110
|
+
continue;
|
|
111
|
+
const entry = byHunter.get(item.bugHunterId) ?? {
|
|
112
|
+
name: item.bugHunterName ?? "Unknown tester",
|
|
113
|
+
count: 0,
|
|
114
|
+
};
|
|
115
|
+
entry.count += 1;
|
|
116
|
+
byHunter.set(item.bugHunterId, entry);
|
|
117
|
+
}
|
|
118
|
+
return Array.from(byHunter.entries())
|
|
119
|
+
.map(([hunterId, v]) => ({ hunterId, hunterName: v.name, bugCount: v.count }))
|
|
120
|
+
.sort((a, b) => b.bugCount - a.bugCount)
|
|
121
|
+
.slice(0, limit)
|
|
122
|
+
.map((entry, index) => ({ ...entry, rank: index + 1 }));
|
|
123
|
+
}
|
|
45
124
|
}
|
|
46
125
|
TesterChecklistItemRepository.SIEVE_FIELDS = {
|
|
47
126
|
groupKey: { canFilter: true, canSort: true },
|
|
@@ -52,5 +131,6 @@ TesterChecklistItemRepository.SIEVE_FIELDS = {
|
|
|
52
131
|
isActive: { canFilter: true, canSort: false },
|
|
53
132
|
searchTokens: { canFilter: true, canSort: false },
|
|
54
133
|
createdAt: { canFilter: true, canSort: true },
|
|
134
|
+
bugConfirmed: { canFilter: true, canSort: false },
|
|
55
135
|
};
|
|
56
136
|
export const testerChecklistItemRepository = new TesterChecklistItemRepository();
|
|
@@ -27,6 +27,10 @@ export type CoverageIssue = TesterChecklistResponseDocument & {
|
|
|
27
27
|
groupLabel: string;
|
|
28
28
|
pageLabel: string;
|
|
29
29
|
label: string;
|
|
30
|
+
bugConfirmed?: boolean;
|
|
31
|
+
bugHunterId?: string;
|
|
32
|
+
bugHunterName?: string;
|
|
33
|
+
supersededByItemId?: string;
|
|
30
34
|
};
|
|
31
35
|
export interface CoverageReport {
|
|
32
36
|
itemCoverage: ChecklistItemCoverage[];
|
|
@@ -105,6 +105,10 @@ export class TesterChecklistResponseRepository extends BaseRepository {
|
|
|
105
105
|
groupLabel,
|
|
106
106
|
pageLabel,
|
|
107
107
|
label: catalogItem?.label ?? doc.checklistItemId,
|
|
108
|
+
bugConfirmed: catalogItem?.bugConfirmed,
|
|
109
|
+
bugHunterId: catalogItem?.bugHunterId,
|
|
110
|
+
bugHunterName: catalogItem?.bugHunterName,
|
|
111
|
+
supersededByItemId: catalogItem?.supersededByItemId,
|
|
108
112
|
});
|
|
109
113
|
}
|
|
110
114
|
}
|
|
@@ -19,18 +19,32 @@ export interface TesterChecklistItemDocument extends BaseDocument {
|
|
|
19
19
|
isActive: boolean;
|
|
20
20
|
searchTokens: string[];
|
|
21
21
|
adminOnly?: boolean;
|
|
22
|
+
bugConfirmed?: boolean;
|
|
23
|
+
bugHunterId?: string;
|
|
24
|
+
bugHunterName?: string;
|
|
25
|
+
bugConfirmedAt?: Date;
|
|
26
|
+
version?: number;
|
|
27
|
+
previousVersionId?: string;
|
|
28
|
+
supersededByItemId?: string;
|
|
22
29
|
}
|
|
23
30
|
export declare const TESTER_CHECKLIST_ITEM_COLLECTION: "testerChecklistItems";
|
|
24
31
|
export declare const TESTER_CHECKLIST_ITEM_ID_PREFIX: "checklist-";
|
|
25
32
|
export declare const TESTER_CHECKLIST_ITEM_INDEXED_FIELDS: readonly ["groupKey", "pageKey", "phase", "order", "isActive", "searchTokens", "createdAt"];
|
|
26
33
|
export declare const DEFAULT_TESTER_CHECKLIST_ITEM_DATA: Partial<TesterChecklistItemDocument>;
|
|
27
34
|
export type TesterChecklistItemCreateInput = Omit<TesterChecklistItemDocument, "id" | "createdAt" | "updatedAt" | "searchTokens">;
|
|
28
|
-
export type TesterChecklistItemUpdateInput = Partial<Pick<TesterChecklistItemDocument, "groupKey" | "groupLabel" | "pageKey" | "pageLabel" | "label" | "description" | "href" | "order" | "phase" | "isActive" | "adminOnly">>;
|
|
35
|
+
export type TesterChecklistItemUpdateInput = Partial<Pick<TesterChecklistItemDocument, "groupKey" | "groupLabel" | "pageKey" | "pageLabel" | "label" | "description" | "href" | "order" | "phase" | "isActive" | "adminOnly" | "bugConfirmed" | "bugHunterId" | "bugHunterName" | "bugConfirmedAt" | "version" | "previousVersionId" | "supersededByItemId">>;
|
|
29
36
|
export declare const testerChecklistItemQueryHelpers: {
|
|
30
37
|
readonly byGroup: (groupKey: string) => readonly ["groupKey", "==", string];
|
|
31
38
|
readonly byPage: (pageKey: string) => readonly ["pageKey", "==", string];
|
|
32
39
|
readonly active: () => readonly ["isActive", "==", true];
|
|
40
|
+
readonly confirmedBugs: () => readonly ["bugConfirmed", "==", true];
|
|
33
41
|
};
|
|
42
|
+
export interface BugHunterLeaderboardEntry {
|
|
43
|
+
rank: number;
|
|
44
|
+
hunterId: string;
|
|
45
|
+
hunterName: string;
|
|
46
|
+
bugCount: number;
|
|
47
|
+
}
|
|
34
48
|
export declare function slugifyChecklistLabel(label: string): string;
|
|
35
49
|
export declare function createChecklistItemId(groupKey: string, pageKey: string, label: string): string;
|
|
36
50
|
export type TesterAnswer = "yes" | "no";
|
|
@@ -72,6 +86,13 @@ export declare const TESTER_CHECKLIST_ITEM_FIELDS: {
|
|
|
72
86
|
readonly IS_ACTIVE: "isActive";
|
|
73
87
|
readonly SEARCH_TOKENS: "searchTokens";
|
|
74
88
|
readonly ADMIN_ONLY: "adminOnly";
|
|
89
|
+
readonly BUG_CONFIRMED: "bugConfirmed";
|
|
90
|
+
readonly BUG_HUNTER_ID: "bugHunterId";
|
|
91
|
+
readonly BUG_HUNTER_NAME: "bugHunterName";
|
|
92
|
+
readonly BUG_CONFIRMED_AT: "bugConfirmedAt";
|
|
93
|
+
readonly VERSION: "version";
|
|
94
|
+
readonly PREVIOUS_VERSION_ID: "previousVersionId";
|
|
95
|
+
readonly SUPERSEDED_BY_ITEM_ID: "supersededByItemId";
|
|
75
96
|
readonly CREATED_AT: "createdAt";
|
|
76
97
|
readonly UPDATED_AT: "updatedAt";
|
|
77
98
|
};
|