@burdenoff/microfe-kadaikodi 2026.710.1 → 2026.716.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.
Files changed (60) hide show
  1. package/dist/kadaikodi/pages/HomePage.d.ts.map +1 -1
  2. package/dist/kadaikodi/pages/HomePage.js.map +1 -1
  3. package/dist/kadaikodi/pages/admin/AdminAnalyticsPage.d.ts.map +1 -1
  4. package/dist/kadaikodi/pages/admin/AdminAnalyticsPage.js.map +1 -1
  5. package/dist/kadaikodi/pages/admin/AdminDashboard.d.ts.map +1 -1
  6. package/dist/kadaikodi/pages/admin/AdminDashboard.js.map +1 -1
  7. package/dist/kadaikodi/pages/admin/AdminFinancialPage.d.ts.map +1 -1
  8. package/dist/kadaikodi/pages/admin/AdminFinancialPage.js.map +1 -1
  9. package/dist/kadaikodi/pages/admin/AdminOrdersPage.d.ts.map +1 -1
  10. package/dist/kadaikodi/pages/admin/AdminOrdersPage.js.map +1 -1
  11. package/dist/kadaikodi/pages/admin/CartManagementPage.d.ts.map +1 -1
  12. package/dist/kadaikodi/pages/admin/CartManagementPage.js.map +1 -1
  13. package/dist/kadaikodi/pages/cart-owner/AnalyticsDashboard.d.ts.map +1 -1
  14. package/dist/kadaikodi/pages/cart-owner/AnalyticsDashboard.js.map +1 -1
  15. package/dist/kadaikodi/pages/cart-owner/InventoryManagementPage.d.ts.map +1 -1
  16. package/dist/kadaikodi/pages/cart-owner/InventoryManagementPage.js.map +1 -1
  17. package/dist/kadaikodi/pages/cart-owner/InvestmentListingPage.js.map +1 -1
  18. package/dist/kadaikodi/pages/cart-owner/MerchantOnboardingPage.d.ts.map +1 -1
  19. package/dist/kadaikodi/pages/cart-owner/MerchantOnboardingPage.js.map +1 -1
  20. package/dist/kadaikodi/pages/cart-owner/OrderManagementPage.d.ts.map +1 -1
  21. package/dist/kadaikodi/pages/cart-owner/OrderManagementPage.js.map +1 -1
  22. package/dist/kadaikodi/pages/cart-owner/StaffManagementPage.d.ts.map +1 -1
  23. package/dist/kadaikodi/pages/cart-owner/StaffManagementPage.js.map +1 -1
  24. package/dist/kadaikodi/pages/customer/BookingDetailsPage.d.ts.map +1 -1
  25. package/dist/kadaikodi/pages/customer/BookingDetailsPage.js.map +1 -1
  26. package/dist/kadaikodi/pages/customer/CartDetailPage.d.ts.map +1 -1
  27. package/dist/kadaikodi/pages/customer/CartDetailPage.js.map +1 -1
  28. package/dist/kadaikodi/pages/customer/CheckoutPage.js.map +1 -1
  29. package/dist/kadaikodi/pages/customer/FavoritesPage.d.ts.map +1 -1
  30. package/dist/kadaikodi/pages/customer/FavoritesPage.js.map +1 -1
  31. package/dist/kadaikodi/pages/customer/OrderTrackingPage.d.ts.map +1 -1
  32. package/dist/kadaikodi/pages/customer/OrderTrackingPage.js.map +1 -1
  33. package/dist/kadaikodi/pages/customer/ReviewsPage.d.ts.map +1 -1
  34. package/dist/kadaikodi/pages/customer/ReviewsPage.js.map +1 -1
  35. package/dist/kadaikodi/pages/customer/shared.d.ts.map +1 -1
  36. package/dist/kadaikodi/pages/customer/shared.js.map +1 -1
  37. package/dist/kadaikodi/pages/investor/InvestmentDetailsPage.d.ts.map +1 -1
  38. package/dist/kadaikodi/pages/investor/InvestmentDetailsPage.js +3 -3
  39. package/dist/kadaikodi/pages/investor/InvestmentDetailsPage.js.map +1 -1
  40. package/dist/kadaikodi/pages/investor/InvestmentOpportunitiesPage.d.ts.map +1 -1
  41. package/dist/kadaikodi/pages/investor/InvestmentOpportunitiesPage.js.map +1 -1
  42. package/dist/kadaikodi/pages/investor/InvestorPortfolioPage.d.ts.map +1 -1
  43. package/dist/kadaikodi/pages/investor/InvestorPortfolioPage.js.map +1 -1
  44. package/dist/kadaikodi/pages/worker/TaskDetailsPage.d.ts.map +1 -1
  45. package/dist/kadaikodi/pages/worker/TaskDetailsPage.js.map +1 -1
  46. package/dist/kadaikodi/pages/worker/WorkerDashboard.d.ts.map +1 -1
  47. package/dist/kadaikodi/pages/worker/WorkerDashboard.js.map +1 -1
  48. package/dist/kadaikodi/pages/worker/WorkerSchedulePage.d.ts.map +1 -1
  49. package/dist/kadaikodi/pages/worker/WorkerSchedulePage.js.map +1 -1
  50. package/dist/services/createSandboxAssistantTransport.d.ts +8 -27
  51. package/dist/services/createSandboxAssistantTransport.d.ts.map +1 -1
  52. package/dist/services/createSandboxAssistantTransport.js +7 -10
  53. package/dist/services/createSandboxAssistantTransport.js.map +1 -1
  54. package/dist/stories/EmptyState.stories.d.ts +10 -0
  55. package/dist/stories/EmptyState.stories.d.ts.map +1 -0
  56. package/dist/stories/MetricsCard.stories.d.ts +10 -0
  57. package/dist/stories/MetricsCard.stories.d.ts.map +1 -0
  58. package/dist/stories/StatusBadge.stories.d.ts +12 -0
  59. package/dist/stories/StatusBadge.stories.d.ts.map +1 -0
  60. package/package.json +15 -3
@@ -1 +1 @@
1
- {"version":3,"file":"ReviewsPage.js","names":[],"sources":["../../../../src/kadaikodi/pages/customer/ReviewsPage.tsx"],"sourcesContent":["/**\n * Reviews Page\n * @module kadaikodi/pages/customer\n *\n * Per-cart reviews on REAL data: pick a cart (defaults to one you've ordered\n * from), see its aggregate rating + a 5→1 rating-distribution, read every\n * review with the owner's public response, and write your own review for a\n * delivered order (persisted via useCreateReview). Nothing here is fabricated —\n * when a cart has no reviews yet, an honest empty state is shown.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Star,\n MessageSquare,\n MessageCircle,\n Search,\n Loader2,\n CheckCircle2,\n PenSquare,\n} from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { RatingStars } from '../../components/viz';\nimport { useKadaikodi } from '../../context/KadaikodiContext';\nimport {\n useProviders,\n useOrders,\n useReviews,\n useCreateReview,\n} from '../../hooks/useKadaikodiApi';\nimport { cn } from '../../utils/cn';\nimport { formatRelativeTime } from '../../utils/formatters';\nimport { ProviderImage } from './shared';\n\nconst ratingLabels: Record<number, string> = {\n 1: 'Terrible',\n 2: 'Poor',\n 3: 'Average',\n 4: 'Good',\n 5: 'Excellent',\n};\n\ntype SortBy = 'recent' | 'rating-high' | 'rating-low';\n\nfunction InteractiveStarRating({\n value,\n onChange,\n}: {\n value: number;\n onChange: (v: number) => void;\n}) {\n const [hoverValue, setHoverValue] = useState(0);\n const displayValue = hoverValue || value;\n return (\n <div className=\"flex items-center gap-3\">\n <div className=\"flex items-center gap-0.5\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button\n key={star}\n type=\"button\"\n onClick={() => onChange(star)}\n onMouseEnter={() => setHoverValue(star)}\n onMouseLeave={() => setHoverValue(0)}\n className=\"p-0.5 transition-transform hover:scale-110\"\n aria-label={`${star} star${star > 1 ? 's' : ''}`}\n >\n <Star\n className={cn(\n 'h-7 w-7',\n star <= displayValue\n ? 'fill-status-warning-text text-status-warning-text'\n : 'fill-none text-text-secondary/40'\n )}\n />\n </button>\n ))}\n </div>\n <span className=\"text-sm font-medium text-text-secondary\">\n {ratingLabels[displayValue] ?? ''}\n </span>\n </div>\n );\n}\n\nexport function ReviewsPage() {\n const { navigateTo, currentUser } = useKadaikodi();\n const { data: providers = [], isLoading: providersLoading } = useProviders();\n const { data: myOrders = [] } = useOrders({ customerId: currentUser?.id });\n const createReview = useCreateReview();\n\n // Delivered orders are the only reviewable ones. The providers behind them\n // are the carts the customer can write about.\n const reviewableOrders = useMemo(\n () => myOrders.filter((o) => o.status === 'delivered'),\n [myOrders]\n );\n const reviewableProviderIds = useMemo(\n () => new Set(reviewableOrders.map((o) => o.cartId)),\n [reviewableOrders]\n );\n\n const [selectedProviderId, setSelectedProviderId] = useState('');\n const effectiveProviderId =\n selectedProviderId ||\n reviewableOrders[0]?.cartId ||\n providers[0]?.id ||\n '';\n\n const provider = useMemo(\n () => providers.find((p) => p.id === effectiveProviderId),\n [providers, effectiveProviderId]\n );\n\n const { data: reviews = [], isLoading: reviewsLoading } = useReviews(effectiveProviderId);\n\n const [searchQuery, setSearchQuery] = useState('');\n const [ratingFilter, setRatingFilter] = useState('');\n const [sortBy, setSortBy] = useState<SortBy>('recent');\n\n const [formRating, setFormRating] = useState(5);\n const [formComment, setFormComment] = useState('');\n const [statusMessage, setStatusMessage] = useState<string | null>(null);\n\n const showStatus = (msg: string): void => {\n setStatusMessage(msg);\n setTimeout(() => setStatusMessage(null), 3000);\n };\n\n // Rating distribution 5→1 from the real review list.\n const distribution = useMemo(() => {\n const counts: Record<number, number> = { 5: 0, 4: 0, 3: 0, 2: 0, 1: 0 };\n for (const r of reviews) {\n const bucket = Math.min(5, Math.max(1, Math.round(r.rating)));\n counts[bucket] += 1;\n }\n return counts;\n }, [reviews]);\n\n const filteredReviews = useMemo(() => {\n let list = [...reviews];\n if (ratingFilter) {\n const val = parseInt(ratingFilter, 10);\n list = list.filter((r) => Math.round(r.rating) === val);\n }\n if (searchQuery.trim()) {\n const q = searchQuery.toLowerCase();\n list = list.filter(\n (r) => (r.review ?? '').toLowerCase().includes(q) || (r.response ?? '').toLowerCase().includes(q)\n );\n }\n switch (sortBy) {\n case 'recent':\n list.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());\n break;\n case 'rating-high':\n list.sort((a, b) => b.rating - a.rating);\n break;\n case 'rating-low':\n list.sort((a, b) => a.rating - b.rating);\n break;\n }\n return list;\n }, [reviews, ratingFilter, searchQuery, sortBy]);\n\n // The delivered order for this provider that anchors a new review.\n const orderForReview = useMemo(\n () => reviewableOrders.find((o) => o.cartId === effectiveProviderId),\n [reviewableOrders, effectiveProviderId]\n );\n const canWrite = Boolean(orderForReview && currentUser?.id);\n\n const handleSubmit = (): void => {\n if (!orderForReview || !currentUser?.id) {\n showStatus('Order from this cart and wait for delivery to leave a review.');\n return;\n }\n createReview.mutate(\n {\n providerId: effectiveProviderId,\n orderId: orderForReview.id,\n customerId: currentUser.id,\n rating: formRating,\n review: formComment.trim() || undefined,\n },\n {\n onSuccess: () => {\n setFormComment('');\n setFormRating(5);\n showStatus('Thanks! Your review has been posted.');\n },\n onError: () => showStatus('Could not post your review. Please try again.'),\n }\n );\n };\n\n if (providersLoading) {\n return (\n <PageLayout title=\"Reviews & Ratings\">\n <div className=\"flex h-[60vh] items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-accent-primary\" />\n </div>\n </PageLayout>\n );\n }\n\n if (!provider) {\n return (\n <PageLayout title=\"Reviews & Ratings\">\n <EmptyState\n icon={<MessageSquare className=\"h-12 w-12\" />}\n title=\"No carts to review yet\"\n description=\"Browse carts and place an order — once it's delivered you can rate and review it.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/customer/discover')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-semibold text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n Discover Carts\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout title=\"Reviews & Ratings\" description={`${reviews.length} reviews`}>\n {statusMessage && (\n <div className=\"mb-4 flex items-center gap-2 rounded-lg border border-status-success-text/30 bg-status-success-bg-subtle p-3\">\n <CheckCircle2 className=\"h-4 w-4 text-status-success-text shrink-0\" />\n <p className=\"text-sm text-status-success-text\">{statusMessage}</p>\n </div>\n )}\n\n <PagePurpose className=\"mb-6\">\n Honest ratings for the carts on Kadaikodi — pick a cart to see its aggregate score and every\n review, then add your own for an order you've had delivered.\n </PagePurpose>\n\n {/* Cart picker */}\n <div className=\"mb-6\">\n <label className=\"mb-1.5 block text-sm font-medium text-text-primary\">\n Viewing reviews for\n </label>\n <select\n value={effectiveProviderId}\n onChange={(e) => setSelectedProviderId(e.target.value)}\n className=\"w-full max-w-sm rounded-lg border border-border-subtle bg-bg-surface px-3 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {providers.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n {reviewableProviderIds.has(p.id) ? ' • ordered' : ''}\n </option>\n ))}\n </select>\n </div>\n\n {/* Aggregate — the one emphasis zone */}\n <EmphasisPanel className=\"mb-6\">\n <div className=\"flex flex-col gap-5 sm:flex-row sm:items-center\">\n <div className=\"flex items-center gap-4\">\n <div className=\"h-16 w-16 shrink-0 overflow-hidden rounded-xl\">\n <ProviderImage src={provider.image} alt={provider.name} seed={provider.name} />\n </div>\n <div>\n <p className=\"text-4xl font-bold leading-none text-text-primary\">\n {provider.rating.toFixed(1)}\n </p>\n <div className=\"mt-1.5\">\n <RatingStars value={provider.rating} size={16} />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">{provider.totalRatings} ratings</p>\n </div>\n </div>\n\n {/* Distribution 5→1 */}\n <div className=\"flex-1 space-y-1.5\">\n {[5, 4, 3, 2, 1].map((star) => {\n const count = distribution[star];\n const pct = reviews.length ? (count / reviews.length) * 100 : 0;\n return (\n <div key={star} className=\"flex items-center gap-2 text-xs\">\n <span className=\"flex w-7 items-center gap-0.5 text-text-secondary\">\n {star}\n <Star className=\"h-3 w-3 fill-status-warning-text text-status-warning-text\" />\n </span>\n <div className=\"h-2 flex-1 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent-primary transition-all\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <span className=\"w-6 text-right tabular-nums text-text-muted\">{count}</span>\n </div>\n );\n })}\n </div>\n </div>\n </EmphasisPanel>\n\n {/* Write a review */}\n <div className=\"mb-6 rounded-xl border border-border-subtle bg-bg-surface p-5\">\n <h2 className=\"mb-3 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <PenSquare className=\"h-4 w-4 text-text-secondary\" />\n Write a review\n </h2>\n {canWrite ? (\n <div className=\"space-y-3\">\n <InteractiveStarRating value={formRating} onChange={setFormRating} />\n <textarea\n rows={3}\n value={formComment}\n onChange={(e) => setFormComment(e.target.value)}\n placeholder=\"Share your experience…\"\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2.5 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] resize-none\"\n />\n <div className=\"flex justify-end\">\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={createReview.isPending}\n className={cn(\n 'inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-semibold text-action-primary-text transition-colors',\n createReview.isPending\n ? 'opacity-60 cursor-not-allowed'\n : 'hover:bg-action-primary-bg-hover'\n )}\n >\n {createReview.isPending && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n Submit Review\n </button>\n </div>\n </div>\n ) : (\n <p className=\"rounded-lg bg-bg-sunken p-3 text-xs text-text-secondary\">\n You can review <span className=\"font-medium text-text-primary\">{provider.name}</span>{' '}\n once one of your orders from it has been delivered.{' '}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/customer/cart/${provider.id}`)}\n className=\"font-medium text-accent-primary hover:underline\"\n >\n Order now\n </button>\n </p>\n )}\n </div>\n\n {/* Filters */}\n <div className=\"mb-6 grid grid-cols-1 gap-3 sm:grid-cols-3\">\n <div className=\"relative sm:col-span-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n placeholder=\"Search reviews…\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n <select\n value={ratingFilter}\n onChange={(e) => setRatingFilter(e.target.value)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"\">All ratings</option>\n <option value=\"5\">5 stars</option>\n <option value=\"4\">4 stars</option>\n <option value=\"3\">3 stars</option>\n <option value=\"2\">2 stars</option>\n <option value=\"1\">1 star</option>\n </select>\n <select\n value={sortBy}\n onChange={(e) => setSortBy(e.target.value as SortBy)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"recent\">Most recent</option>\n <option value=\"rating-high\">Highest rating</option>\n <option value=\"rating-low\">Lowest rating</option>\n </select>\n </div>\n\n {/* Review list */}\n {reviewsLoading ? (\n <div className=\"flex h-40 items-center justify-center\">\n <Loader2 className=\"h-6 w-6 animate-spin text-accent-primary\" />\n </div>\n ) : filteredReviews.length === 0 ? (\n <EmptyState\n icon={<MessageSquare className=\"h-12 w-12\" />}\n title=\"No reviews yet\"\n description={`Be the first to review ${provider.name}.`}\n />\n ) : (\n <div className=\"space-y-3\">\n {filteredReviews.map((review) => {\n const isMine = review.customerId === currentUser?.id;\n return (\n <div\n key={review.id}\n className={cn(\n 'rounded-xl border bg-bg-surface p-4',\n isMine ? 'border-action-primary-bg/40' : 'border-border-subtle'\n )}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-2\">\n <RatingStars value={review.rating} size={14} />\n {isMine && (\n <span className=\"rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-[10px] font-medium text-accent-primary\">\n Your review\n </span>\n )}\n </div>\n <span className=\"text-xs text-text-muted\">\n {formatRelativeTime(review.createdAt)}\n </span>\n </div>\n {review.review && (\n <p className=\"mt-2 text-sm leading-relaxed text-text-primary\">{review.review}</p>\n )}\n {review.response && (\n <div className=\"mt-3 rounded-lg border-l-2 border-accent-primary bg-[var(--color-accent-soft)] p-3\">\n <p className=\"mb-1 flex items-center gap-1.5 text-xs font-medium text-text-primary\">\n <MessageCircle className=\"h-3.5 w-3.5\" />\n Owner response\n </p>\n <p className=\"text-xs leading-relaxed text-text-primary\">{review.response}</p>\n </div>\n )}\n </div>\n );\n })}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ReviewsPage.js","names":[],"sources":["../../../../src/kadaikodi/pages/customer/ReviewsPage.tsx"],"sourcesContent":["/**\n * Reviews Page\n * @module kadaikodi/pages/customer\n *\n * Per-cart reviews on REAL data: pick a cart (defaults to one you've ordered\n * from), see its aggregate rating + a 5→1 rating-distribution, read every\n * review with the owner's public response, and write your own review for a\n * delivered order (persisted via useCreateReview). Nothing here is fabricated —\n * when a cart has no reviews yet, an honest empty state is shown.\n */\n\nimport { useMemo, useState } from 'react';\nimport {\n Star,\n MessageSquare,\n MessageCircle,\n Search,\n Loader2,\n CheckCircle2,\n PenSquare,\n} from 'lucide-react';\nimport { EmphasisPanel, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { RatingStars } from '../../components/viz';\nimport { useKadaikodi } from '../../context/KadaikodiContext';\nimport { useProviders, useOrders, useReviews, useCreateReview } from '../../hooks/useKadaikodiApi';\nimport { cn } from '../../utils/cn';\nimport { formatRelativeTime } from '../../utils/formatters';\nimport { ProviderImage } from './shared';\n\nconst ratingLabels: Record<number, string> = {\n 1: 'Terrible',\n 2: 'Poor',\n 3: 'Average',\n 4: 'Good',\n 5: 'Excellent',\n};\n\ntype SortBy = 'recent' | 'rating-high' | 'rating-low';\n\nfunction InteractiveStarRating({\n value,\n onChange,\n}: {\n value: number;\n onChange: (v: number) => void;\n}) {\n const [hoverValue, setHoverValue] = useState(0);\n const displayValue = hoverValue || value;\n return (\n <div className=\"flex items-center gap-3\">\n <div className=\"flex items-center gap-0.5\">\n {[1, 2, 3, 4, 5].map((star) => (\n <button\n key={star}\n type=\"button\"\n onClick={() => onChange(star)}\n onMouseEnter={() => setHoverValue(star)}\n onMouseLeave={() => setHoverValue(0)}\n className=\"p-0.5 transition-transform hover:scale-110\"\n aria-label={`${star} star${star > 1 ? 's' : ''}`}\n >\n <Star\n className={cn(\n 'h-7 w-7',\n star <= displayValue\n ? 'fill-status-warning-text text-status-warning-text'\n : 'fill-none text-text-secondary/40'\n )}\n />\n </button>\n ))}\n </div>\n <span className=\"text-sm font-medium text-text-secondary\">\n {ratingLabels[displayValue] ?? ''}\n </span>\n </div>\n );\n}\n\nexport function ReviewsPage() {\n const { navigateTo, currentUser } = useKadaikodi();\n const { data: providers = [], isLoading: providersLoading } = useProviders();\n const { data: myOrders = [] } = useOrders({ customerId: currentUser?.id });\n const createReview = useCreateReview();\n\n // Delivered orders are the only reviewable ones. The providers behind them\n // are the carts the customer can write about.\n const reviewableOrders = useMemo(\n () => myOrders.filter((o) => o.status === 'delivered'),\n [myOrders]\n );\n const reviewableProviderIds = useMemo(\n () => new Set(reviewableOrders.map((o) => o.cartId)),\n [reviewableOrders]\n );\n\n const [selectedProviderId, setSelectedProviderId] = useState('');\n const effectiveProviderId =\n selectedProviderId || reviewableOrders[0]?.cartId || providers[0]?.id || '';\n\n const provider = useMemo(\n () => providers.find((p) => p.id === effectiveProviderId),\n [providers, effectiveProviderId]\n );\n\n const { data: reviews = [], isLoading: reviewsLoading } = useReviews(effectiveProviderId);\n\n const [searchQuery, setSearchQuery] = useState('');\n const [ratingFilter, setRatingFilter] = useState('');\n const [sortBy, setSortBy] = useState<SortBy>('recent');\n\n const [formRating, setFormRating] = useState(5);\n const [formComment, setFormComment] = useState('');\n const [statusMessage, setStatusMessage] = useState<string | null>(null);\n\n const showStatus = (msg: string): void => {\n setStatusMessage(msg);\n setTimeout(() => setStatusMessage(null), 3000);\n };\n\n // Rating distribution 5→1 from the real review list.\n const distribution = useMemo(() => {\n const counts: Record<number, number> = { 5: 0, 4: 0, 3: 0, 2: 0, 1: 0 };\n for (const r of reviews) {\n const bucket = Math.min(5, Math.max(1, Math.round(r.rating)));\n counts[bucket] += 1;\n }\n return counts;\n }, [reviews]);\n\n const filteredReviews = useMemo(() => {\n let list = [...reviews];\n if (ratingFilter) {\n const val = parseInt(ratingFilter, 10);\n list = list.filter((r) => Math.round(r.rating) === val);\n }\n if (searchQuery.trim()) {\n const q = searchQuery.toLowerCase();\n list = list.filter(\n (r) =>\n (r.review ?? '').toLowerCase().includes(q) || (r.response ?? '').toLowerCase().includes(q)\n );\n }\n switch (sortBy) {\n case 'recent':\n list.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());\n break;\n case 'rating-high':\n list.sort((a, b) => b.rating - a.rating);\n break;\n case 'rating-low':\n list.sort((a, b) => a.rating - b.rating);\n break;\n }\n return list;\n }, [reviews, ratingFilter, searchQuery, sortBy]);\n\n // The delivered order for this provider that anchors a new review.\n const orderForReview = useMemo(\n () => reviewableOrders.find((o) => o.cartId === effectiveProviderId),\n [reviewableOrders, effectiveProviderId]\n );\n const canWrite = Boolean(orderForReview && currentUser?.id);\n\n const handleSubmit = (): void => {\n if (!orderForReview || !currentUser?.id) {\n showStatus('Order from this cart and wait for delivery to leave a review.');\n return;\n }\n createReview.mutate(\n {\n providerId: effectiveProviderId,\n orderId: orderForReview.id,\n customerId: currentUser.id,\n rating: formRating,\n review: formComment.trim() || undefined,\n },\n {\n onSuccess: () => {\n setFormComment('');\n setFormRating(5);\n showStatus('Thanks! Your review has been posted.');\n },\n onError: () => showStatus('Could not post your review. Please try again.'),\n }\n );\n };\n\n if (providersLoading) {\n return (\n <PageLayout title=\"Reviews & Ratings\">\n <div className=\"flex h-[60vh] items-center justify-center\">\n <Loader2 className=\"h-8 w-8 animate-spin text-accent-primary\" />\n </div>\n </PageLayout>\n );\n }\n\n if (!provider) {\n return (\n <PageLayout title=\"Reviews & Ratings\">\n <EmptyState\n icon={<MessageSquare className=\"h-12 w-12\" />}\n title=\"No carts to review yet\"\n description=\"Browse carts and place an order — once it's delivered you can rate and review it.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/customer/discover')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-semibold text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n Discover Carts\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout title=\"Reviews & Ratings\" description={`${reviews.length} reviews`}>\n {statusMessage && (\n <div className=\"mb-4 flex items-center gap-2 rounded-lg border border-status-success-text/30 bg-status-success-bg-subtle p-3\">\n <CheckCircle2 className=\"h-4 w-4 text-status-success-text shrink-0\" />\n <p className=\"text-sm text-status-success-text\">{statusMessage}</p>\n </div>\n )}\n\n <PagePurpose className=\"mb-6\">\n Honest ratings for the carts on Kadaikodi — pick a cart to see its aggregate score and every\n review, then add your own for an order you've had delivered.\n </PagePurpose>\n\n {/* Cart picker */}\n <div className=\"mb-6\">\n <label className=\"mb-1.5 block text-sm font-medium text-text-primary\">\n Viewing reviews for\n </label>\n <select\n value={effectiveProviderId}\n onChange={(e) => setSelectedProviderId(e.target.value)}\n className=\"w-full max-w-sm rounded-lg border border-border-subtle bg-bg-surface px-3 py-2.5 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n {providers.map((p) => (\n <option key={p.id} value={p.id}>\n {p.name}\n {reviewableProviderIds.has(p.id) ? ' • ordered' : ''}\n </option>\n ))}\n </select>\n </div>\n\n {/* Aggregate — the one emphasis zone */}\n <EmphasisPanel className=\"mb-6\">\n <div className=\"flex flex-col gap-5 sm:flex-row sm:items-center\">\n <div className=\"flex items-center gap-4\">\n <div className=\"h-16 w-16 shrink-0 overflow-hidden rounded-xl\">\n <ProviderImage src={provider.image} alt={provider.name} seed={provider.name} />\n </div>\n <div>\n <p className=\"text-4xl font-bold leading-none text-text-primary\">\n {provider.rating.toFixed(1)}\n </p>\n <div className=\"mt-1.5\">\n <RatingStars value={provider.rating} size={16} />\n </div>\n <p className=\"mt-1 text-xs text-text-secondary\">{provider.totalRatings} ratings</p>\n </div>\n </div>\n\n {/* Distribution 5→1 */}\n <div className=\"flex-1 space-y-1.5\">\n {[5, 4, 3, 2, 1].map((star) => {\n const count = distribution[star];\n const pct = reviews.length ? (count / reviews.length) * 100 : 0;\n return (\n <div key={star} className=\"flex items-center gap-2 text-xs\">\n <span className=\"flex w-7 items-center gap-0.5 text-text-secondary\">\n {star}\n <Star className=\"h-3 w-3 fill-status-warning-text text-status-warning-text\" />\n </span>\n <div className=\"h-2 flex-1 overflow-hidden rounded-full bg-bg-sunken\">\n <div\n className=\"h-full rounded-full bg-accent-primary transition-all\"\n style={{ width: `${pct}%` }}\n />\n </div>\n <span className=\"w-6 text-right tabular-nums text-text-muted\">{count}</span>\n </div>\n );\n })}\n </div>\n </div>\n </EmphasisPanel>\n\n {/* Write a review */}\n <div className=\"mb-6 rounded-xl border border-border-subtle bg-bg-surface p-5\">\n <h2 className=\"mb-3 flex items-center gap-2 text-base font-semibold text-text-primary\">\n <PenSquare className=\"h-4 w-4 text-text-secondary\" />\n Write a review\n </h2>\n {canWrite ? (\n <div className=\"space-y-3\">\n <InteractiveStarRating value={formRating} onChange={setFormRating} />\n <textarea\n rows={3}\n value={formComment}\n onChange={(e) => setFormComment(e.target.value)}\n placeholder=\"Share your experience…\"\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2.5 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] resize-none\"\n />\n <div className=\"flex justify-end\">\n <button\n type=\"button\"\n onClick={handleSubmit}\n disabled={createReview.isPending}\n className={cn(\n 'inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-semibold text-action-primary-text transition-colors',\n createReview.isPending\n ? 'opacity-60 cursor-not-allowed'\n : 'hover:bg-action-primary-bg-hover'\n )}\n >\n {createReview.isPending && <Loader2 className=\"h-4 w-4 animate-spin\" />}\n Submit Review\n </button>\n </div>\n </div>\n ) : (\n <p className=\"rounded-lg bg-bg-sunken p-3 text-xs text-text-secondary\">\n You can review <span className=\"font-medium text-text-primary\">{provider.name}</span>{' '}\n once one of your orders from it has been delivered.{' '}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/customer/cart/${provider.id}`)}\n className=\"font-medium text-accent-primary hover:underline\"\n >\n Order now\n </button>\n </p>\n )}\n </div>\n\n {/* Filters */}\n <div className=\"mb-6 grid grid-cols-1 gap-3 sm:grid-cols-3\">\n <div className=\"relative sm:col-span-1\">\n <Search className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n type=\"text\"\n placeholder=\"Search reviews…\"\n value={searchQuery}\n onChange={(e) => setSearchQuery(e.target.value)}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2 pl-9 pr-3 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n <select\n value={ratingFilter}\n onChange={(e) => setRatingFilter(e.target.value)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"\">All ratings</option>\n <option value=\"5\">5 stars</option>\n <option value=\"4\">4 stars</option>\n <option value=\"3\">3 stars</option>\n <option value=\"2\">2 stars</option>\n <option value=\"1\">1 star</option>\n </select>\n <select\n value={sortBy}\n onChange={(e) => setSortBy(e.target.value as SortBy)}\n className=\"rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n >\n <option value=\"recent\">Most recent</option>\n <option value=\"rating-high\">Highest rating</option>\n <option value=\"rating-low\">Lowest rating</option>\n </select>\n </div>\n\n {/* Review list */}\n {reviewsLoading ? (\n <div className=\"flex h-40 items-center justify-center\">\n <Loader2 className=\"h-6 w-6 animate-spin text-accent-primary\" />\n </div>\n ) : filteredReviews.length === 0 ? (\n <EmptyState\n icon={<MessageSquare className=\"h-12 w-12\" />}\n title=\"No reviews yet\"\n description={`Be the first to review ${provider.name}.`}\n />\n ) : (\n <div className=\"space-y-3\">\n {filteredReviews.map((review) => {\n const isMine = review.customerId === currentUser?.id;\n return (\n <div\n key={review.id}\n className={cn(\n 'rounded-xl border bg-bg-surface p-4',\n isMine ? 'border-action-primary-bg/40' : 'border-border-subtle'\n )}\n >\n <div className=\"flex items-center justify-between gap-2\">\n <div className=\"flex items-center gap-2\">\n <RatingStars value={review.rating} size={14} />\n {isMine && (\n <span className=\"rounded-full bg-[var(--color-accent-soft)] px-2 py-0.5 text-[10px] font-medium text-accent-primary\">\n Your review\n </span>\n )}\n </div>\n <span className=\"text-xs text-text-muted\">\n {formatRelativeTime(review.createdAt)}\n </span>\n </div>\n {review.review && (\n <p className=\"mt-2 text-sm leading-relaxed text-text-primary\">{review.review}</p>\n )}\n {review.response && (\n <div className=\"mt-3 rounded-lg border-l-2 border-accent-primary bg-[var(--color-accent-soft)] p-3\">\n <p className=\"mb-1 flex items-center gap-1.5 text-xs font-medium text-text-primary\">\n <MessageCircle className=\"h-3.5 w-3.5\" />\n Owner response\n </p>\n <p className=\"text-xs leading-relaxed text-text-primary\">{review.response}</p>\n </div>\n )}\n </div>\n );\n })}\n </div>\n )}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../../src/kadaikodi/pages/customer/shared.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAQf,UAAU,sBAAsB;IAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,sBAAsB,+BAgC5F;AAED,qDAAqD;AACrD,wBAAgB,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,CAAC,+BAExE;AAuBD,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAM/C;AAcD,4DAA4D;AAC5D,wBAAgB,YAAY,IAAI;IAC9B,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7B,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B,CAKA;AAMD,UAAU,qBAAqB;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAc,EACd,WAAoB,EACpB,gBAAwB,EACxB,SAAS,GACV,EAAE,qBAAqB,+BAoEvB"}
1
+ {"version":3,"file":"shared.d.ts","sourceRoot":"","sources":["../../../../src/kadaikodi/pages/customer/shared.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAwD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAQ7F,UAAU,sBAAsB;IAC9B,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,gEAAgE;IAChE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gEAAgE;IAChE,IAAI,CAAC,EAAE,SAAS,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,sBAAsB,+BAgC5F;AAED,qDAAqD;AACrD,wBAAgB,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,sBAAsB,EAAE,MAAM,CAAC,+BAExE;AAuBD,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAM/C;AAcD,4DAA4D;AAC5D,wBAAgB,YAAY,IAAI;IAC9B,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,GAAG,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7B,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B,CAKA;AAMD,UAAU,qBAAqB;IAC7B,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAC/B,QAAc,EACd,WAAoB,EACpB,gBAAwB,EACxB,SAAS,GACV,EAAE,qBAAqB,+BAgFvB"}
@@ -1 +1 @@
1
- {"version":3,"file":"shared.js","names":[],"sources":["../../../../src/kadaikodi/pages/customer/shared.tsx"],"sourcesContent":["/**\n * Shared presentational helpers for the customer pages.\n * @module kadaikodi/pages/customer\n *\n * - `ProviderImage` / `OfferingImage`: render a real media image when the\n * backend supplies one, otherwise a tokenized, themeable placeholder (never a\n * broken <img> and never a fabricated stock photo).\n * - `useFavorites`: a real, user-owned favorites store persisted in\n * localStorage and shared reactively across the customer pages (Discover adds,\n * Favorites reads). This is genuine user data — nothing is fabricated.\n * - `StylizedRouteMap`: an honest, clearly-labelled illustrative route panel for\n * order tracking (there is no live-GPS backend, so we never fake live coords).\n */\n\nimport {\n useCallback,\n useMemo,\n useState,\n useSyncExternalStore,\n type ReactNode,\n} from 'react';\nimport { Store, MapPin, Navigation, Home } from 'lucide-react';\nimport { cn } from '../../utils/cn';\n\n// ============================================================================\n// Media with graceful, tokenized fallback\n// ============================================================================\n\ninterface ImageWithFallbackProps {\n src?: string | null;\n alt: string;\n /** Seed text for the placeholder initial (usually the name). */\n seed?: string;\n className?: string;\n /** Icon shown in the placeholder (defaults to a storefront). */\n icon?: ReactNode;\n}\n\n/**\n * Renders `src` when present and loadable; otherwise a tokenized accent-gradient\n * placeholder with the seed's initial. Fills its positioned parent.\n */\nexport function ImageWithFallback({ src, alt, seed, className, icon }: ImageWithFallbackProps) {\n const [failed, setFailed] = useState(false);\n const showImage = Boolean(src) && !failed;\n\n if (showImage) {\n return (\n <img\n src={src as string}\n alt={alt}\n loading=\"lazy\"\n onError={() => setFailed(true)}\n className={cn('h-full w-full object-cover', className)}\n />\n );\n }\n\n const initial = (seed ?? alt).trim().charAt(0).toUpperCase() || '?';\n return (\n <div\n aria-label={alt}\n role=\"img\"\n className={cn(\n 'flex h-full w-full items-center justify-center bg-gradient-to-br from-accent-primary/25 via-accent-primary/10 to-bg-sunken',\n className\n )}\n >\n <div className=\"flex flex-col items-center gap-1 text-accent-primary/70\">\n {icon ?? <Store className=\"h-6 w-6\" aria-hidden=\"true\" />}\n <span className=\"text-lg font-bold leading-none\">{initial}</span>\n </div>\n </div>\n );\n}\n\n/** Convenience wrapper for provider/cart imagery. */\nexport function ProviderImage(props: Omit<ImageWithFallbackProps, 'icon'>) {\n return <ImageWithFallback {...props} icon={<Store className=\"h-6 w-6\" aria-hidden=\"true\" />} />;\n}\n\n// ============================================================================\n// Favorites — real localStorage-backed store, reactive across pages\n// ============================================================================\n\nconst FAVORITES_KEY = 'kadaikodi:favorites:providers';\nconst listeners = new Set<() => void>();\n\nfunction readRaw(): string {\n if (typeof localStorage === 'undefined') return '[]';\n return localStorage.getItem(FAVORITES_KEY) ?? '[]';\n}\n\nfunction parse(raw: string): string[] {\n try {\n const parsed: unknown = JSON.parse(raw);\n return Array.isArray(parsed) ? parsed.filter((x): x is string => typeof x === 'string') : [];\n } catch {\n return [];\n }\n}\n\n/** Toggle a provider id in the favorites store and notify subscribers. */\nexport function toggleFavorite(id: string): void {\n if (typeof localStorage === 'undefined') return;\n const current = parse(readRaw());\n const next = current.includes(id) ? current.filter((x) => x !== id) : [...current, id];\n localStorage.setItem(FAVORITES_KEY, JSON.stringify(next));\n listeners.forEach((l) => l());\n}\n\nconst subscribe = (cb: () => void): (() => void) => {\n listeners.add(cb);\n const onStorage = (e: StorageEvent): void => {\n if (e.key === FAVORITES_KEY) cb();\n };\n if (typeof window !== 'undefined') window.addEventListener('storage', onStorage);\n return () => {\n listeners.delete(cb);\n if (typeof window !== 'undefined') window.removeEventListener('storage', onStorage);\n };\n};\n\n/** Reactive access to the customer's saved-provider ids. */\nexport function useFavorites(): {\n ids: string[];\n has: (id: string) => boolean;\n toggle: (id: string) => void;\n} {\n const raw = useSyncExternalStore(subscribe, readRaw, () => '[]');\n const ids = useMemo(() => parse(raw), [raw]);\n const has = useCallback((id: string) => ids.includes(id), [ids]);\n return { ids, has, toggle: toggleFavorite };\n}\n\n// ============================================================================\n// Stylised route map (honest illustration — no live GPS backend)\n// ============================================================================\n\ninterface StylizedRouteMapProps {\n /** 0–1 progress of the courier along the illustrative route. */\n progress?: number;\n originLabel?: string;\n destinationLabel?: string;\n className?: string;\n}\n\n/**\n * A tokenized, animated-dashed SVG \"route\" between the cart and the delivery\n * address. It is explicitly labelled as illustrative — Kadaikodi has no live-GPS\n * feed, so this never pretends to show a real courier position.\n */\nexport function StylizedRouteMap({\n progress = 0.5,\n originLabel = 'Cart',\n destinationLabel = 'You',\n className,\n}: StylizedRouteMapProps) {\n const clamped = Math.min(1, Math.max(0, progress));\n // Path points in the 0..400 x 0..220 viewBox.\n const start = { x: 60, y: 168 };\n const end = { x: 340, y: 56 };\n const c1 = { x: 150, y: 60 };\n const c2 = { x: 250, y: 200 };\n const t = clamped;\n const mt = 1 - t;\n // Cubic bezier point at t for the courier marker.\n const cx =\n mt * mt * mt * start.x +\n 3 * mt * mt * t * c1.x +\n 3 * mt * t * t * c2.x +\n t * t * t * end.x;\n const cy =\n mt * mt * mt * start.y +\n 3 * mt * mt * t * c1.y +\n 3 * mt * t * t * c2.y +\n t * t * t * end.y;\n\n return (\n <div\n className={cn(\n 'relative overflow-hidden rounded-xl border border-border-subtle bg-bg-sunken',\n className\n )}\n >\n <svg viewBox=\"0 0 400 220\" className=\"h-full w-full\" role=\"img\" aria-label=\"Illustrative delivery route\">\n {/* Faint \"streets\" grid */}\n <g stroke=\"var(--color-border-subtle)\" strokeWidth=\"1\" opacity=\"0.6\">\n <path d=\"M0 40 H400 M0 90 H400 M0 140 H400 M0 190 H400\" />\n <path d=\"M70 0 V220 M150 0 V220 M230 0 V220 M310 0 V220\" />\n </g>\n {/* Route */}\n <path\n d={`M${start.x} ${start.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${end.x} ${end.y}`}\n fill=\"none\"\n stroke=\"var(--color-accent-primary)\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n strokeDasharray=\"2 8\"\n opacity=\"0.9\"\n />\n {/* Origin (cart) */}\n <circle cx={start.x} cy={start.y} r=\"16\" fill=\"var(--color-accent-primary)\" opacity=\"0.12\" />\n <circle cx={start.x} cy={start.y} r=\"6\" fill=\"var(--color-accent-primary)\" />\n {/* Destination (home) */}\n <circle cx={end.x} cy={end.y} r=\"9\" fill=\"var(--color-status-success-text)\" />\n {/* Courier marker */}\n <circle cx={cx} cy={cy} r=\"12\" fill=\"var(--color-accent-primary)\" opacity=\"0.18\">\n <animate attributeName=\"r\" values=\"10;16;10\" dur=\"2.4s\" repeatCount=\"indefinite\" />\n </circle>\n <circle cx={cx} cy={cy} r=\"6\" fill=\"var(--color-accent-primary)\" stroke=\"var(--color-bg-surface)\" strokeWidth=\"2\" />\n </svg>\n\n {/* Corner labels */}\n <span className=\"absolute bottom-2 left-2 inline-flex items-center gap-1 rounded-full bg-bg-surface/80 px-2 py-0.5 text-[10px] font-medium text-text-secondary backdrop-blur-sm\">\n <MapPin className=\"h-3 w-3 text-accent-primary\" /> {originLabel}\n </span>\n <span className=\"absolute right-2 top-2 inline-flex items-center gap-1 rounded-full bg-bg-surface/80 px-2 py-0.5 text-[10px] font-medium text-text-secondary backdrop-blur-sm\">\n <Home className=\"h-3 w-3 text-status-success-text\" /> {destinationLabel}\n </span>\n <span className=\"absolute right-2 bottom-2 inline-flex items-center gap-1 rounded-full bg-bg-surface/70 px-2 py-0.5 text-[10px] text-text-muted backdrop-blur-sm\">\n <Navigation className=\"h-3 w-3\" /> Illustrative route\n </span>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"shared.js","names":[],"sources":["../../../../src/kadaikodi/pages/customer/shared.tsx"],"sourcesContent":["/**\n * Shared presentational helpers for the customer pages.\n * @module kadaikodi/pages/customer\n *\n * - `ProviderImage` / `OfferingImage`: render a real media image when the\n * backend supplies one, otherwise a tokenized, themeable placeholder (never a\n * broken <img> and never a fabricated stock photo).\n * - `useFavorites`: a real, user-owned favorites store persisted in\n * localStorage and shared reactively across the customer pages (Discover adds,\n * Favorites reads). This is genuine user data — nothing is fabricated.\n * - `StylizedRouteMap`: an honest, clearly-labelled illustrative route panel for\n * order tracking (there is no live-GPS backend, so we never fake live coords).\n */\n\nimport { useCallback, useMemo, useState, useSyncExternalStore, type ReactNode } from 'react';\nimport { Store, MapPin, Navigation, Home } from 'lucide-react';\nimport { cn } from '../../utils/cn';\n\n// ============================================================================\n// Media with graceful, tokenized fallback\n// ============================================================================\n\ninterface ImageWithFallbackProps {\n src?: string | null;\n alt: string;\n /** Seed text for the placeholder initial (usually the name). */\n seed?: string;\n className?: string;\n /** Icon shown in the placeholder (defaults to a storefront). */\n icon?: ReactNode;\n}\n\n/**\n * Renders `src` when present and loadable; otherwise a tokenized accent-gradient\n * placeholder with the seed's initial. Fills its positioned parent.\n */\nexport function ImageWithFallback({ src, alt, seed, className, icon }: ImageWithFallbackProps) {\n const [failed, setFailed] = useState(false);\n const showImage = Boolean(src) && !failed;\n\n if (showImage) {\n return (\n <img\n src={src as string}\n alt={alt}\n loading=\"lazy\"\n onError={() => setFailed(true)}\n className={cn('h-full w-full object-cover', className)}\n />\n );\n }\n\n const initial = (seed ?? alt).trim().charAt(0).toUpperCase() || '?';\n return (\n <div\n aria-label={alt}\n role=\"img\"\n className={cn(\n 'flex h-full w-full items-center justify-center bg-gradient-to-br from-accent-primary/25 via-accent-primary/10 to-bg-sunken',\n className\n )}\n >\n <div className=\"flex flex-col items-center gap-1 text-accent-primary/70\">\n {icon ?? <Store className=\"h-6 w-6\" aria-hidden=\"true\" />}\n <span className=\"text-lg font-bold leading-none\">{initial}</span>\n </div>\n </div>\n );\n}\n\n/** Convenience wrapper for provider/cart imagery. */\nexport function ProviderImage(props: Omit<ImageWithFallbackProps, 'icon'>) {\n return <ImageWithFallback {...props} icon={<Store className=\"h-6 w-6\" aria-hidden=\"true\" />} />;\n}\n\n// ============================================================================\n// Favorites — real localStorage-backed store, reactive across pages\n// ============================================================================\n\nconst FAVORITES_KEY = 'kadaikodi:favorites:providers';\nconst listeners = new Set<() => void>();\n\nfunction readRaw(): string {\n if (typeof localStorage === 'undefined') return '[]';\n return localStorage.getItem(FAVORITES_KEY) ?? '[]';\n}\n\nfunction parse(raw: string): string[] {\n try {\n const parsed: unknown = JSON.parse(raw);\n return Array.isArray(parsed) ? parsed.filter((x): x is string => typeof x === 'string') : [];\n } catch {\n return [];\n }\n}\n\n/** Toggle a provider id in the favorites store and notify subscribers. */\nexport function toggleFavorite(id: string): void {\n if (typeof localStorage === 'undefined') return;\n const current = parse(readRaw());\n const next = current.includes(id) ? current.filter((x) => x !== id) : [...current, id];\n localStorage.setItem(FAVORITES_KEY, JSON.stringify(next));\n listeners.forEach((l) => l());\n}\n\nconst subscribe = (cb: () => void): (() => void) => {\n listeners.add(cb);\n const onStorage = (e: StorageEvent): void => {\n if (e.key === FAVORITES_KEY) cb();\n };\n if (typeof window !== 'undefined') window.addEventListener('storage', onStorage);\n return () => {\n listeners.delete(cb);\n if (typeof window !== 'undefined') window.removeEventListener('storage', onStorage);\n };\n};\n\n/** Reactive access to the customer's saved-provider ids. */\nexport function useFavorites(): {\n ids: string[];\n has: (id: string) => boolean;\n toggle: (id: string) => void;\n} {\n const raw = useSyncExternalStore(subscribe, readRaw, () => '[]');\n const ids = useMemo(() => parse(raw), [raw]);\n const has = useCallback((id: string) => ids.includes(id), [ids]);\n return { ids, has, toggle: toggleFavorite };\n}\n\n// ============================================================================\n// Stylised route map (honest illustration — no live GPS backend)\n// ============================================================================\n\ninterface StylizedRouteMapProps {\n /** 0–1 progress of the courier along the illustrative route. */\n progress?: number;\n originLabel?: string;\n destinationLabel?: string;\n className?: string;\n}\n\n/**\n * A tokenized, animated-dashed SVG \"route\" between the cart and the delivery\n * address. It is explicitly labelled as illustrative — Kadaikodi has no live-GPS\n * feed, so this never pretends to show a real courier position.\n */\nexport function StylizedRouteMap({\n progress = 0.5,\n originLabel = 'Cart',\n destinationLabel = 'You',\n className,\n}: StylizedRouteMapProps) {\n const clamped = Math.min(1, Math.max(0, progress));\n // Path points in the 0..400 x 0..220 viewBox.\n const start = { x: 60, y: 168 };\n const end = { x: 340, y: 56 };\n const c1 = { x: 150, y: 60 };\n const c2 = { x: 250, y: 200 };\n const t = clamped;\n const mt = 1 - t;\n // Cubic bezier point at t for the courier marker.\n const cx =\n mt * mt * mt * start.x + 3 * mt * mt * t * c1.x + 3 * mt * t * t * c2.x + t * t * t * end.x;\n const cy =\n mt * mt * mt * start.y + 3 * mt * mt * t * c1.y + 3 * mt * t * t * c2.y + t * t * t * end.y;\n\n return (\n <div\n className={cn(\n 'relative overflow-hidden rounded-xl border border-border-subtle bg-bg-sunken',\n className\n )}\n >\n <svg\n viewBox=\"0 0 400 220\"\n className=\"h-full w-full\"\n role=\"img\"\n aria-label=\"Illustrative delivery route\"\n >\n {/* Faint \"streets\" grid */}\n <g stroke=\"var(--color-border-subtle)\" strokeWidth=\"1\" opacity=\"0.6\">\n <path d=\"M0 40 H400 M0 90 H400 M0 140 H400 M0 190 H400\" />\n <path d=\"M70 0 V220 M150 0 V220 M230 0 V220 M310 0 V220\" />\n </g>\n {/* Route */}\n <path\n d={`M${start.x} ${start.y} C ${c1.x} ${c1.y}, ${c2.x} ${c2.y}, ${end.x} ${end.y}`}\n fill=\"none\"\n stroke=\"var(--color-accent-primary)\"\n strokeWidth=\"3\"\n strokeLinecap=\"round\"\n strokeDasharray=\"2 8\"\n opacity=\"0.9\"\n />\n {/* Origin (cart) */}\n <circle\n cx={start.x}\n cy={start.y}\n r=\"16\"\n fill=\"var(--color-accent-primary)\"\n opacity=\"0.12\"\n />\n <circle cx={start.x} cy={start.y} r=\"6\" fill=\"var(--color-accent-primary)\" />\n {/* Destination (home) */}\n <circle cx={end.x} cy={end.y} r=\"9\" fill=\"var(--color-status-success-text)\" />\n {/* Courier marker */}\n <circle cx={cx} cy={cy} r=\"12\" fill=\"var(--color-accent-primary)\" opacity=\"0.18\">\n <animate attributeName=\"r\" values=\"10;16;10\" dur=\"2.4s\" repeatCount=\"indefinite\" />\n </circle>\n <circle\n cx={cx}\n cy={cy}\n r=\"6\"\n fill=\"var(--color-accent-primary)\"\n stroke=\"var(--color-bg-surface)\"\n strokeWidth=\"2\"\n />\n </svg>\n\n {/* Corner labels */}\n <span className=\"absolute bottom-2 left-2 inline-flex items-center gap-1 rounded-full bg-bg-surface/80 px-2 py-0.5 text-[10px] font-medium text-text-secondary backdrop-blur-sm\">\n <MapPin className=\"h-3 w-3 text-accent-primary\" /> {originLabel}\n </span>\n <span className=\"absolute right-2 top-2 inline-flex items-center gap-1 rounded-full bg-bg-surface/80 px-2 py-0.5 text-[10px] font-medium text-text-secondary backdrop-blur-sm\">\n <Home className=\"h-3 w-3 text-status-success-text\" /> {destinationLabel}\n </span>\n <span className=\"absolute right-2 bottom-2 inline-flex items-center gap-1 rounded-full bg-bg-surface/70 px-2 py-0.5 text-[10px] text-text-muted backdrop-blur-sm\">\n <Navigation className=\"h-3 w-3\" /> Illustrative route\n </span>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"InvestmentDetailsPage.d.ts","sourceRoot":"","sources":["../../../../src/kadaikodi/pages/investor/InvestmentDetailsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAsEH,wBAAgB,qBAAqB,gCA+dpC"}
1
+ {"version":3,"file":"InvestmentDetailsPage.d.ts","sourceRoot":"","sources":["../../../../src/kadaikodi/pages/investor/InvestmentDetailsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAsEH,wBAAgB,qBAAqB,gCAoepC"}
@@ -439,10 +439,10 @@ function D() {
439
439
  children: [
440
440
  "Forecast at ",
441
441
  I?.expectedReturns,
442
- "% over ",
443
- I?.investmentPeriod,
442
+ "% over",
444
443
  " ",
445
- "months"
444
+ I?.investmentPeriod,
445
+ " months"
446
446
  ]
447
447
  }),
448
448
  /* @__PURE__ */ w(s, {
@@ -1 +1 @@
1
- {"version":3,"file":"InvestmentDetailsPage.js","names":[],"sources":["../../../../src/kadaikodi/pages/investor/InvestmentDetailsPage.tsx"],"sourcesContent":["/**\n * Investment Details Page\n * @module kadaikodi/pages/investor\n *\n * Serves both /investor/opportunity/:id and /investor/investments/:id. Resolves\n * the opportunity (and the investor's holding, if any) from the live backend by\n * route id. Business metrics are read from the real service-provider record\n * (useProvider by cartId) — never fabricated. The invest panel is wired to the\n * live useInvest mutation; the projected-returns curve is a transparent forecast\n * computed from the stated expected return, not invented performance data.\n */\n\nimport { useCallback, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n TrendingUp,\n Shield,\n Users,\n IndianRupee,\n Star,\n MessageSquare,\n Receipt,\n Tag,\n CheckCircle2,\n Lock,\n Target,\n} from 'lucide-react';\nimport { EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { AreaChart, ProgressRing, StatCard } from '../../components/viz';\nimport { useKadaikodi } from '../../context/KadaikodiContext';\nimport {\n useInvestmentOpportunities,\n useInvestments,\n useInvest,\n useProvider,\n} from '../../hooks/useKadaikodiApi';\nimport { cn } from '../../utils/cn';\nimport { formatCurrency, formatDate } from '../../utils/formatters';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction getRiskConfig(risk: string): { label: string; color: string; badge: string } {\n switch (risk) {\n case 'low':\n return {\n label: 'Low risk',\n color: 'text-status-success-text',\n badge: 'border-status-success-border bg-status-success-bg-subtle text-status-success-text',\n };\n case 'medium':\n return {\n label: 'Medium risk',\n color: 'text-status-warning-text',\n badge: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n };\n case 'high':\n return {\n label: 'High risk',\n color: 'text-status-error-text',\n badge: 'border-status-error-border bg-status-error-bg-subtle text-status-error-text',\n };\n default:\n return { label: risk, color: 'text-text-secondary', badge: 'border-border-default' };\n }\n}\n\n/** Compact rupee formatter for chart axes / tooltips. */\nfunction formatRupeeCompact(value: number): string {\n return formatCurrency(value, { minimumFractionDigits: 0, notation: 'compact' });\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function InvestmentDetailsPage() {\n const { navigateTo, currentUser } = useKadaikodi();\n const { id } = useParams<{ id: string }>();\n\n const { data: opportunities = [], isLoading: opportunitiesLoading } =\n useInvestmentOpportunities();\n const { data: investments = [], isLoading: investmentsLoading } = useInvestments(\n currentUser?.id || ''\n );\n\n const isLoading = opportunitiesLoading || investmentsLoading;\n\n // The route id may be an investment id or an opportunity id. Resolve the\n // holding first (if it belongs to this investor), then the opportunity.\n const investment = useMemo(() => investments.find((inv) => inv.id === id), [investments, id]);\n const opportunity = useMemo(\n () =>\n opportunities.find((opp) => opp.id === id || opp.id === investment?.opportunityId) ?? null,\n [opportunities, id, investment]\n );\n\n // Real business metrics come from the underlying service-provider record.\n const { data: provider } = useProvider(opportunity?.cartId);\n\n // Invest panel state (wired to the live mutation) ------------------------\n const invest = useInvest();\n const [amount, setAmount] = useState('');\n const [investStatus, setInvestStatus] = useState<'idle' | 'success' | 'error'>('idle');\n\n const minInvestment = opportunity?.minimumInvestment ?? 0;\n const remaining = opportunity ? Math.max(0, opportunity.targetAmount - opportunity.raisedAmount) : 0;\n const maxInvestment = Math.max(minInvestment, remaining);\n const parsedAmount = Number.parseInt(amount, 10);\n const amountValid =\n !Number.isNaN(parsedAmount) && parsedAmount >= minInvestment && parsedAmount <= maxInvestment;\n\n const handleInvest = useCallback(() => {\n if (!opportunity || !amountValid) {\n setInvestStatus('error');\n return;\n }\n invest.mutate(\n { amount: parsedAmount, investorId: currentUser?.id ?? '', opportunityId: opportunity.id },\n {\n onSuccess: () => {\n setInvestStatus('success');\n window.setTimeout(() => navigateTo('/investor/portfolio'), 1400);\n },\n onError: () => setInvestStatus('error'),\n }\n );\n }, [opportunity, amountValid, invest, parsedAmount, currentUser?.id, navigateTo]);\n\n // Projected-returns forecast: a transparent linear glide from the amount put\n // in today to amount × (1 + expected return) at the end of the lock-in. It is\n // a computed projection from the stated rate, clearly labelled — not data.\n const projection = useMemo(() => {\n if (!opportunity) return [];\n const base = amountValid ? parsedAmount : minInvestment || opportunity.targetAmount / 10;\n const finalValue = base * (1 + opportunity.expectedReturns / 100);\n const months = Math.min(Math.max(opportunity.investmentPeriod, 3), 12);\n const steps = 6;\n return Array.from({ length: steps + 1 }, (_, i) => {\n const monthLabel =\n i === 0 ? 'Now' : i === steps ? `${months}mo` : `M${Math.round((months * i) / steps)}`;\n return { label: monthLabel, value: base + (finalValue - base) * (i / steps) };\n });\n }, [opportunity, amountValid, parsedAmount, minInvestment]);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Investment Details\" description=\"Loading...\">\n <div className=\"space-y-4\">\n <div className=\"h-28 animate-pulse rounded-[var(--radius-card)] bg-bg-sunken\" />\n <div className=\"grid gap-6 lg:grid-cols-3\">\n <div className=\"h-96 animate-pulse rounded-[var(--radius-card)] bg-bg-sunken lg:col-span-2\" />\n <div className=\"h-96 animate-pulse rounded-[var(--radius-card)] bg-bg-sunken\" />\n </div>\n </div>\n </PageLayout>\n );\n }\n\n if (!opportunity && !investment) {\n return (\n <PageLayout title=\"Investment Not Found\">\n <EmptyState\n illustration=\"empty-generic\"\n title=\"Investment not found\"\n description=\"We couldn't find this investment. It may have been closed or the link is out of date.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/investor/portfolio')}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-accent transition-colors\"\n >\n Back to Portfolio\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n const cartName = opportunity?.cartName ?? investment?.cartName ?? 'Investment';\n const risk = opportunity ? getRiskConfig(opportunity.riskLevel) : null;\n const fundingPct =\n opportunity && opportunity.targetAmount > 0\n ? Math.min((opportunity.raisedAmount / opportunity.targetAmount) * 100, 100)\n : 0;\n const daysLeft = opportunity?.expiresAt\n ? Math.max(0, Math.ceil((new Date(opportunity.expiresAt).getTime() - Date.now()) / 86400000))\n : null;\n\n const holdingReturns = investment ? investment.currentValue - investment.investedAmount : 0;\n const holdingReturnsPct =\n investment && investment.investedAmount > 0\n ? (holdingReturns / investment.investedAmount) * 100\n : 0;\n\n const canInvest = Boolean(opportunity) && opportunity?.status === 'active';\n\n return (\n <PageLayout\n title={cartName}\n description=\"Investment details & performance\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/investor/portfolio')}\n className=\"flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-accent transition-colors\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n <span className=\"hidden sm:inline\">Back to Portfolio</span>\n <span className=\"sm:hidden\">Back</span>\n </button>\n }\n >\n <PagePurpose className=\"mb-6\">\n A deep dive into this investment. Review the business behind it, funding progress, expected\n returns and terms on offer, and — if you already hold a stake — exactly what you&apos;ve put\n in and what it&apos;s worth today.\n </PagePurpose>\n\n {/* Existing holding summary — the one emphasis zone when you own a stake */}\n {investment && (\n <EmphasisPanel>\n <h2 className=\"mb-4 text-base font-semibold text-text-primary\">Your Position</h2>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <StatCard\n label=\"Your Investment\"\n value={formatCurrency(investment.investedAmount, { minimumFractionDigits: 0 })}\n icon={<IndianRupee className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Current Value\"\n value={formatCurrency(investment.currentValue, { minimumFractionDigits: 0 })}\n icon={<TrendingUp className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Returns\"\n value={`${holdingReturnsPct >= 0 ? '+' : ''}${holdingReturnsPct.toFixed(1)}%`}\n delta={Number(holdingReturnsPct.toFixed(1))}\n deltaLabel=\"on invested\"\n icon={<TrendingUp className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Realised Returns\"\n value={formatCurrency(investment.returns, { minimumFractionDigits: 0 })}\n icon={<Receipt className=\"h-4 w-4\" />}\n />\n </div>\n </EmphasisPanel>\n )}\n\n <div className=\"mt-6 grid gap-6 lg:grid-cols-3\">\n {/* Left — the business & funding story */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* Hero header with risk badge */}\n {opportunity && (\n <GlassCard treatment=\"glass\" className=\"p-5 sm:p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"text-sm text-text-secondary\">{opportunity.businessOwner}</p>\n <h2 className=\"mt-0.5 text-xl font-bold tracking-tight text-text-primary sm:text-2xl\">\n {opportunity.cartName}\n </h2>\n </div>\n {risk && (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-sm font-semibold',\n risk.badge\n )}\n >\n <Shield className=\"h-4 w-4\" />\n {risk.label}\n </span>\n )}\n </div>\n {opportunity.description && (\n <p className=\"mt-4 text-sm leading-relaxed text-text-secondary\">\n {opportunity.description}\n </p>\n )}\n </GlassCard>\n )}\n\n {/* Business Metrics — real provider figures */}\n {provider && (\n <section>\n <h3 className=\"mb-3 text-lg font-semibold text-text-primary\">Business Metrics</h3>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-4 sm:gap-4\">\n <StatCard\n label=\"Rating\"\n value={provider.rating > 0 ? provider.rating.toFixed(1) : '—'}\n icon={<Star className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Reviews\"\n value={provider.totalRatings.toLocaleString('en-IN')}\n icon={<MessageSquare className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Avg Order\"\n value={\n provider.averageOrderValue\n ? formatCurrency(provider.averageOrderValue, { minimumFractionDigits: 0 })\n : '—'\n }\n icon={<Receipt className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Category\"\n value={\n <span className=\"text-lg font-semibold capitalize\">\n {provider.category || '—'}\n </span>\n }\n icon={<Tag className=\"h-4 w-4\" />}\n />\n </div>\n </section>\n )}\n\n {/* Funding progress with a ProgressRing */}\n {opportunity && (\n <GlassCard treatment=\"plain\" className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 text-lg font-semibold text-text-primary\">Funding Progress</h3>\n <div className=\"flex flex-col items-center gap-6 sm:flex-row sm:gap-8\">\n <ProgressRing\n value={fundingPct}\n size={140}\n label={`${fundingPct.toFixed(0)}%`}\n caption=\"funded\"\n />\n <div className=\"w-full flex-1 space-y-4\">\n <div>\n <div className=\"flex items-baseline justify-between\">\n <span className=\"text-2xl font-bold text-text-primary\">\n {formatCurrency(opportunity.raisedAmount, { minimumFractionDigits: 0 })}\n </span>\n <span className=\"text-sm text-text-secondary\">\n of {formatCurrency(opportunity.targetAmount, { minimumFractionDigits: 0 })}\n </span>\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">raised so far</p>\n </div>\n <div className=\"grid grid-cols-3 gap-3 border-t border-border-subtle pt-4 text-center\">\n <div>\n <p className=\"text-base font-bold text-text-primary sm:text-lg\">\n {opportunity.investors.length}\n </p>\n <p className=\"text-xs text-text-secondary\">Investors</p>\n </div>\n <div>\n <p className=\"text-base font-bold text-text-primary sm:text-lg\">\n {formatCurrency(remaining, { minimumFractionDigits: 0, notation: 'compact' })}\n </p>\n <p className=\"text-xs text-text-secondary\">To Go</p>\n </div>\n <div>\n <p className=\"text-base font-bold text-text-primary sm:text-lg\">\n {daysLeft !== null ? `${daysLeft}` : '—'}\n </p>\n <p className=\"text-xs text-text-secondary\">Days Left</p>\n </div>\n </div>\n </div>\n </div>\n </GlassCard>\n )}\n\n {/* Your holding detail */}\n {investment && (\n <GlassCard treatment=\"plain\" className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 text-lg font-semibold text-text-primary\">Your Holding</h3>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div>\n <p className=\"text-sm text-text-secondary\">Invested Amount</p>\n <p className=\"font-medium text-text-primary\">\n {formatCurrency(investment.investedAmount, { minimumFractionDigits: 0 })}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Current Value</p>\n <p className=\"font-medium text-text-primary\">\n {formatCurrency(investment.currentValue, { minimumFractionDigits: 0 })}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Returns Realised</p>\n <p className=\"font-medium text-text-primary\">\n {formatCurrency(investment.returns, { minimumFractionDigits: 0 })}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Invested On</p>\n <p className=\"font-medium text-text-primary\">\n {formatDate(investment.investmentDate, { hour: undefined, minute: undefined })}\n </p>\n </div>\n </div>\n </GlassCard>\n )}\n </div>\n\n {/* Right — sticky invest panel */}\n <div className=\"space-y-6\">\n <div className=\"lg:sticky lg:top-6\">\n <GlassCard treatment=\"glass\" glow className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {canInvest ? 'Invest' : 'Investment Terms'}\n </h3>\n\n {/* Terms */}\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <span className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <TrendingUp className=\"h-4 w-4\" />\n Expected returns\n </span>\n <span className=\"font-semibold text-status-success-text\">\n {opportunity?.expectedReturns ?? investment?.returnsPercentage}% p.a.\n </span>\n </div>\n <div className=\"flex items-center justify-between\">\n <span className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Lock className=\"h-4 w-4\" />\n Lock-in\n </span>\n <span className=\"font-semibold text-text-primary\">\n {opportunity?.investmentPeriod ?? '—'} months\n </span>\n </div>\n {opportunity && (\n <div className=\"flex items-center justify-between\">\n <span className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <IndianRupee className=\"h-4 w-4\" />\n Minimum\n </span>\n <span className=\"font-semibold text-text-primary\">\n {formatCurrency(opportunity.minimumInvestment, { minimumFractionDigits: 0 })}\n </span>\n </div>\n )}\n {opportunity && opportunity.investors.length > 0 && (\n <div className=\"flex items-center justify-between\">\n <span className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Users className=\"h-4 w-4\" />\n Investors\n </span>\n <span className=\"font-semibold text-text-primary\">\n {opportunity.investors.length}\n </span>\n </div>\n )}\n </div>\n\n {canInvest && investStatus !== 'success' && (\n <div className=\"mt-5 space-y-4 border-t border-border-subtle pt-5\">\n <div>\n <label\n htmlFor=\"invest-amount\"\n className=\"mb-1.5 block text-sm font-medium text-text-primary\"\n >\n Amount\n </label>\n <div className=\"relative\">\n <IndianRupee className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n id=\"invest-amount\"\n type=\"number\"\n inputMode=\"numeric\"\n value={amount}\n onChange={(e) => {\n setAmount(e.target.value);\n setInvestStatus('idle');\n }}\n placeholder={String(minInvestment)}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2.5 pl-9 pr-3 text-base font-semibold text-text-primary placeholder:font-normal placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n <p className=\"mt-1.5 text-xs text-text-secondary\">\n {formatCurrency(minInvestment, { minimumFractionDigits: 0 })} –{' '}\n {formatCurrency(maxInvestment, { minimumFractionDigits: 0 })}\n </p>\n {investStatus === 'error' && (\n <p className=\"mt-1 text-xs text-status-error-text\">\n Enter an amount between{' '}\n {formatCurrency(minInvestment, { minimumFractionDigits: 0 })} and{' '}\n {formatCurrency(maxInvestment, { minimumFractionDigits: 0 })}.\n </p>\n )}\n </div>\n\n {/* Projected returns forecast */}\n <div>\n <p className=\"mb-1 text-sm font-medium text-text-primary\">Projected returns</p>\n <p className=\"mb-2 text-xs text-text-muted\">\n Forecast at {opportunity?.expectedReturns}% over {opportunity?.investmentPeriod}{' '}\n months\n </p>\n <AreaChart\n series={projection}\n height={140}\n valueFormatter={formatRupeeCompact}\n title=\"Projected value over the lock-in period\"\n />\n {amountValid && (\n <div className=\"mt-2 flex items-center justify-between rounded-lg bg-bg-sunken/60 px-3 py-2 text-sm\">\n <span className=\"text-text-secondary\">Projected value</span>\n <span className=\"font-semibold text-status-success-text\">\n {formatCurrency(\n parsedAmount * (1 + (opportunity?.expectedReturns ?? 0) / 100),\n { minimumFractionDigits: 0 }\n )}\n </span>\n </div>\n )}\n </div>\n\n <button\n type=\"button\"\n onClick={handleInvest}\n disabled={!amountValid || invest.isPending}\n className={cn(\n 'flex w-full items-center justify-center gap-2 rounded-lg px-4 py-3 text-sm font-semibold transition-colors',\n amountValid && !invest.isPending\n ? 'bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover'\n : 'cursor-not-allowed bg-bg-sunken text-text-secondary'\n )}\n >\n {invest.isPending ? 'Processing…' : 'Invest Now'}\n </button>\n </div>\n )}\n\n {investStatus === 'success' && (\n <div className=\"mt-5 border-t border-border-subtle pt-5 text-center\">\n <CheckCircle2 className=\"mx-auto mb-2 h-10 w-10 text-status-success-text\" />\n <p className=\"text-sm font-medium text-text-primary\">Investment successful!</p>\n <p className=\"mt-1 text-xs text-text-secondary\">Redirecting to your portfolio…</p>\n </div>\n )}\n\n {!canInvest && !investment && opportunity && (\n <div className=\"mt-5 flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-sunken/50 p-3 text-sm text-text-secondary\">\n <Target className=\"h-4 w-4 shrink-0\" />\n This opportunity is {opportunity.status}. It&apos;s no longer accepting\n investments.\n </div>\n )}\n </GlassCard>\n </div>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA8CA,SAAS,GAAc,GAA+D;AACpF,SAAQ,GAAR;EACE,KAAK,MACH,QAAO;GACL,OAAO;GACP,OAAO;GACP,OAAO;GACR;EACH,KAAK,SACH,QAAO;GACL,OAAO;GACP,OAAO;GACP,OAAO;GACR;EACH,KAAK,OACH,QAAO;GACL,OAAO;GACP,OAAO;GACP,OAAO;GACR;EACH,QACE,QAAO;GAAE,OAAO;GAAM,OAAO;GAAuB,OAAO;GAAyB;;;AAK1F,SAAS,GAAmB,GAAuB;AACjD,QAAO,EAAe,GAAO;EAAE,uBAAuB;EAAG,UAAU;EAAW,CAAC;;AAOjF,SAAgB,IAAwB;CACtC,IAAM,EAAE,eAAY,mBAAgB,GAAc,EAC5C,EAAE,UAAO,GAA2B,EAEpC,EAAE,MAAM,IAAgB,EAAE,EAAE,WAAW,MAC3C,GAA4B,EACxB,EAAE,MAAM,IAAc,EAAE,EAAE,WAAW,MAAuB,EAChE,GAAa,MAAM,GACpB,EAEK,IAAY,KAAwB,GAIpC,IAAa,QAAc,EAAY,MAAM,MAAQ,EAAI,OAAO,EAAG,EAAE,CAAC,GAAa,EAAG,CAAC,EACvF,IAAc,QAEhB,EAAc,MAAM,MAAQ,EAAI,OAAO,KAAM,EAAI,OAAO,GAAY,cAAc,IAAI,MACxF;EAAC;EAAe;EAAI;EAAW,CAChC,EAGK,EAAE,MAAM,MAAa,GAAY,GAAa,OAAO,EAGrD,IAAS,GAAW,EACpB,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAc,KAAmB,EAAuC,OAAO,EAEhF,IAAgB,GAAa,qBAAqB,GAClD,IAAY,IAAc,KAAK,IAAI,GAAG,EAAY,eAAe,EAAY,aAAa,GAAG,GAC7F,IAAgB,KAAK,IAAI,GAAe,EAAU,EAClD,IAAe,OAAO,SAAS,GAAQ,GAAG,EAC1C,IACJ,CAAC,OAAO,MAAM,EAAa,IAAI,KAAgB,KAAiB,KAAgB,GAE5E,KAAe,QAAkB;AACrC,MAAI,CAAC,KAAe,CAAC,GAAa;AAChC,KAAgB,QAAQ;AACxB;;AAEF,IAAO,OACL;GAAE,QAAQ;GAAc,YAAY,GAAa,MAAM;GAAI,eAAe,EAAY;GAAI,EAC1F;GACE,iBAAiB;AAEf,IADA,EAAgB,UAAU,EAC1B,OAAO,iBAAiB,EAAW,sBAAsB,EAAE,KAAK;;GAElE,eAAe,EAAgB,QAAQ;GACxC,CACF;IACA;EAAC;EAAa;EAAa;EAAQ;EAAc,GAAa;EAAI;EAAW,CAAC,EAK3E,IAAa,QAAc;AAC/B,MAAI,CAAC,EAAa,QAAO,EAAE;EAC3B,IAAM,IAAO,IAAc,IAAe,KAAiB,EAAY,eAAe,IAChF,IAAa,KAAQ,IAAI,EAAY,kBAAkB,MACvD,IAAS,KAAK,IAAI,KAAK,IAAI,EAAY,kBAAkB,EAAE,EAAE,GAAG;AAEtE,SAAO,MAAM,KAAK,EAAE,QAAQ,GAAW,GAAG,IAAG,OAGpC;GAAE,OADP,MAAM,IAAI,QAAQ,MAAM,IAAQ,GAAG,EAAO,MAAM,IAAI,KAAK,MAAO,IAAS,IAAK,EAAM;GAC1D,OAAO,KAAQ,IAAa,MAAS,IAAI;GAAQ,EAC7E;IACD;EAAC;EAAa;EAAa;EAAc;EAAc,CAAC;AAE3D,KAAI,EACF,QACE,kBAAC,GAAD;EAAY,OAAM;EAAqB,aAAY;YACjD,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,OAAD,EAAK,WAAU,gEAAiE,CAAA,EAChF,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAK,WAAU,8EAA+E,CAAA,EAC9F,kBAAC,OAAD,EAAK,WAAU,gEAAiE,CAAA,CAC5E;MACF;;EACK,CAAA;AAIjB,KAAI,CAAC,KAAe,CAAC,EACnB,QACE,kBAAC,GAAD;EAAY,OAAM;YAChB,kBAAC,IAAD;GACE,cAAa;GACb,OAAM;GACN,aAAY;GACZ,QACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAW,sBAAsB;IAChD,WAAU;cACX;IAEQ,CAAA;GAEX,CAAA;EACS,CAAA;CAIjB,IAAM,KAAW,GAAa,YAAY,GAAY,YAAY,cAC5D,IAAO,IAAc,GAAc,EAAY,UAAU,GAAG,MAC5D,IACJ,KAAe,EAAY,eAAe,IACtC,KAAK,IAAK,EAAY,eAAe,EAAY,eAAgB,KAAK,IAAI,GAC1E,GACA,IAAW,GAAa,YAC1B,KAAK,IAAI,GAAG,KAAK,MAAM,IAAI,KAAK,EAAY,UAAU,CAAC,SAAS,GAAG,KAAK,KAAK,IAAI,MAAS,CAAC,GAC3F,MAEE,KAAiB,IAAa,EAAW,eAAe,EAAW,iBAAiB,GACpF,IACJ,KAAc,EAAW,iBAAiB,IACrC,KAAiB,EAAW,iBAAkB,MAC/C,GAEA,IAAY,EAAQ,KAAgB,GAAa,WAAW;AAElE,QACE,kBAAC,GAAD;EACE,OAAO;EACP,aAAY;EACZ,SACE,kBAAC,UAAD;GACE,MAAK;GACL,eAAe,EAAW,sBAAsB;GAChD,WAAU;aAHZ;IAKE,kBAAC,IAAD,EAAW,WAAU,WAAY,CAAA;IACjC,kBAAC,QAAD;KAAM,WAAU;eAAmB;KAAwB,CAAA;IAC3D,kBAAC,QAAD;KAAM,WAAU;eAAY;KAAW,CAAA;IAChC;;YAZb;GAeE,kBAAC,IAAD;IAAa,WAAU;cAAO;IAIhB,CAAA;GAGb,KACC,kBAAC,IAAD,EAAA,UAAA,CACE,kBAAC,MAAD;IAAI,WAAU;cAAiD;IAAkB,CAAA,EACjF,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,EAAe,EAAW,gBAAgB,EAAE,uBAAuB,GAAG,CAAC;MAC9E,MAAM,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA;MACzC,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,EAAe,EAAW,cAAc,EAAE,uBAAuB,GAAG,CAAC;MAC5E,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;MACxC,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,GAAG,KAAqB,IAAI,MAAM,KAAK,EAAkB,QAAQ,EAAE,CAAC;MAC3E,OAAO,OAAO,EAAkB,QAAQ,EAAE,CAAC;MAC3C,YAAW;MACX,MAAM,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA;MACxC,CAAA;KACF,kBAAC,GAAD;MACE,OAAM;MACN,OAAO,EAAe,EAAW,SAAS,EAAE,uBAAuB,GAAG,CAAC;MACvE,MAAM,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;MACrC,CAAA;KACE;MACQ,EAAA,CAAA;GAGlB,kBAAC,OAAD;IAAK,WAAU;cAAf,CAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MAEG,KACC,kBAAC,GAAD;OAAW,WAAU;OAAQ,WAAU;iBAAvC,CACE,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,KAAD;UAAG,WAAU;oBAA+B,EAAY;UAAkB,CAAA,EAC1E,kBAAC,MAAD;UAAI,WAAU;oBACX,EAAY;UACV,CAAA,CACD;YACL,KACC,kBAAC,QAAD;SACE,WAAW,EACT,wFACA,EAAK,MACN;mBAJH,CAME,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA,EAC7B,EAAK,MACD;WAEL;WACL,EAAY,eACX,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAY;QACX,CAAA,CAEI;;MAIb,KACC,kBAAC,WAAD,EAAA,UAAA,CACE,kBAAC,MAAD;OAAI,WAAU;iBAA+C;OAAqB,CAAA,EAClF,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,GAAD;SACE,OAAM;SACN,OAAO,EAAS,SAAS,IAAI,EAAS,OAAO,QAAQ,EAAE,GAAG;SAC1D,MAAM,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA;SAClC,CAAA;QACF,kBAAC,GAAD;SACE,OAAM;SACN,OAAO,EAAS,aAAa,eAAe,QAAQ;SACpD,MAAM,kBAAC,GAAD,EAAe,WAAU,WAAY,CAAA;SAC3C,CAAA;QACF,kBAAC,GAAD;SACE,OAAM;SACN,OACE,EAAS,oBACL,EAAe,EAAS,mBAAmB,EAAE,uBAAuB,GAAG,CAAC,GACxE;SAEN,MAAM,kBAAC,GAAD,EAAS,WAAU,WAAY,CAAA;SACrC,CAAA;QACF,kBAAC,GAAD;SACE,OAAM;SACN,OACE,kBAAC,QAAD;UAAM,WAAU;oBACb,EAAS,YAAY;UACjB,CAAA;SAET,MAAM,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA;SACjC,CAAA;QACE;SACE,EAAA,CAAA;MAIX,KACC,kBAAC,GAAD;OAAW,WAAU;OAAQ,WAAU;iBAAvC,CACE,kBAAC,MAAD;QAAI,WAAU;kBAA+C;QAAqB,CAAA,EAClF,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD;SACE,OAAO;SACP,MAAM;SACN,OAAO,GAAG,EAAW,QAAQ,EAAE,CAAC;SAChC,SAAQ;SACR,CAAA,EACF,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,OAAD;UAAK,WAAU;oBAAf,CACE,kBAAC,QAAD;WAAM,WAAU;qBACb,EAAe,EAAY,cAAc,EAAE,uBAAuB,GAAG,CAAC;WAClE,CAAA,EACP,kBAAC,QAAD;WAAM,WAAU;qBAAhB,CAA8C,OACxC,EAAe,EAAY,cAAc,EAAE,uBAAuB,GAAG,CAAC,CACrE;aACH;aACN,kBAAC,KAAD;UAAG,WAAU;oBAAmC;UAAiB,CAAA,CAC7D,EAAA,CAAA,EACN,kBAAC,OAAD;UAAK,WAAU;oBAAf;WACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAY,UAAU;YACrB,CAAA,EACJ,kBAAC,KAAD;YAAG,WAAU;sBAA8B;YAAa,CAAA,CACpD,EAAA,CAAA;WACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;YAAG,WAAU;sBACV,EAAe,GAAW;aAAE,uBAAuB;aAAG,UAAU;aAAW,CAAC;YAC3E,CAAA,EACJ,kBAAC,KAAD;YAAG,WAAU;sBAA8B;YAAS,CAAA,CAChD,EAAA,CAAA;WACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;YAAG,WAAU;sBACV,MAAa,OAAuB,MAAhB,GAAG;YACtB,CAAA,EACJ,kBAAC,KAAD;YAAG,WAAU;sBAA8B;YAAa,CAAA,CACpD,EAAA,CAAA;WACF;YACF;WACF;UACI;;MAIb,KACC,kBAAC,GAAD;OAAW,WAAU;OAAQ,WAAU;iBAAvC,CACE,kBAAC,MAAD;QAAI,WAAU;kBAA+C;QAAiB,CAAA,EAC9E,kBAAC,OAAD;QAAK,WAAU;kBAAf;SACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;UAAG,WAAU;oBAA8B;UAAmB,CAAA,EAC9D,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAe,EAAW,gBAAgB,EAAE,uBAAuB,GAAG,CAAC;UACtE,CAAA,CACA,EAAA,CAAA;SACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;UAAG,WAAU;oBAA8B;UAAiB,CAAA,EAC5D,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAe,EAAW,cAAc,EAAE,uBAAuB,GAAG,CAAC;UACpE,CAAA,CACA,EAAA,CAAA;SACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;UAAG,WAAU;oBAA8B;UAAoB,CAAA,EAC/D,kBAAC,KAAD;UAAG,WAAU;oBACV,EAAe,EAAW,SAAS,EAAE,uBAAuB,GAAG,CAAC;UAC/D,CAAA,CACA,EAAA,CAAA;SACN,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,KAAD;UAAG,WAAU;oBAA8B;UAAe,CAAA,EAC1D,kBAAC,KAAD;UAAG,WAAU;oBACV,GAAW,EAAW,gBAAgB;WAAE,MAAM,KAAA;WAAW,QAAQ,KAAA;WAAW,CAAC;UAC5E,CAAA,CACA,EAAA,CAAA;SACF;UACI;;MAEV;QAGN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,OAAD;MAAK,WAAU;gBACb,kBAAC,GAAD;OAAW,WAAU;OAAQ,MAAA;OAAK,WAAU;iBAA5C;QACE,kBAAC,MAAD;SAAI,WAAU;mBACX,IAAY,WAAW;SACrB,CAAA;QAGL,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,QAAD;YAAM,WAAU;sBAAhB,CACE,kBAAC,GAAD,EAAY,WAAU,WAAY,CAAA,EAAA,mBAE7B;eACP,kBAAC,QAAD;YAAM,WAAU;sBAAhB,CACG,GAAa,mBAAmB,GAAY,mBAAkB,SAC1D;cACH;;UACN,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,QAAD;YAAM,WAAU;sBAAhB,CACE,kBAAC,IAAD,EAAM,WAAU,WAAY,CAAA,EAAA,UAEvB;eACP,kBAAC,QAAD;YAAM,WAAU;sBAAhB,CACG,GAAa,oBAAoB,KAAI,UACjC;cACH;;UACL,KACC,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,QAAD;YAAM,WAAU;sBAAhB,CACE,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA,EAAA,UAE9B;eACP,kBAAC,QAAD;YAAM,WAAU;sBACb,EAAe,EAAY,mBAAmB,EAAE,uBAAuB,GAAG,CAAC;YACvE,CAAA,CACH;;UAEP,KAAe,EAAY,UAAU,SAAS,KAC7C,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,QAAD;YAAM,WAAU;sBAAhB,CACE,kBAAC,GAAD,EAAO,WAAU,WAAY,CAAA,EAAA,YAExB;eACP,kBAAC,QAAD;YAAM,WAAU;sBACb,EAAY,UAAU;YAClB,CAAA,CACH;;UAEJ;;QAEL,KAAa,MAAiB,aAC7B,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,OAAD,EAAA,UAAA;WACE,kBAAC,SAAD;YACE,SAAQ;YACR,WAAU;sBACX;YAEO,CAAA;WACR,kBAAC,OAAD;YAAK,WAAU;sBAAf,CACE,kBAAC,GAAD,EAAa,WAAU,wEAAyE,CAAA,EAChG,kBAAC,SAAD;aACE,IAAG;aACH,MAAK;aACL,WAAU;aACV,OAAO;aACP,WAAW,MAAM;AAEf,cADA,EAAU,EAAE,OAAO,MAAM,EACzB,EAAgB,OAAO;;aAEzB,aAAa,OAAO,EAAc;aAClC,WAAU;aACV,CAAA,CACE;;WACN,kBAAC,KAAD;YAAG,WAAU;sBAAb;aACG,EAAe,GAAe,EAAE,uBAAuB,GAAG,CAAC;aAAC;aAAG;aAC/D,EAAe,GAAe,EAAE,uBAAuB,GAAG,CAAC;aAC1D;;WACH,MAAiB,WAChB,kBAAC,KAAD;YAAG,WAAU;sBAAb;aAAmD;aACzB;aACvB,EAAe,GAAe,EAAE,uBAAuB,GAAG,CAAC;aAAC;aAAK;aACjE,EAAe,GAAe,EAAE,uBAAuB,GAAG,CAAC;aAAC;aAC3D;;WAEF,EAAA,CAAA;UAGN,kBAAC,OAAD,EAAA,UAAA;WACE,kBAAC,KAAD;YAAG,WAAU;sBAA6C;YAAqB,CAAA;WAC/E,kBAAC,KAAD;YAAG,WAAU;sBAAb;aAA4C;aAC7B,GAAa;aAAgB;aAAQ,GAAa;aAAkB;aAAI;aAEnF;;WACJ,kBAAC,GAAD;YACE,QAAQ;YACR,QAAQ;YACR,gBAAgB;YAChB,OAAM;YACN,CAAA;WACD,KACC,kBAAC,OAAD;YAAK,WAAU;sBAAf,CACE,kBAAC,QAAD;aAAM,WAAU;uBAAsB;aAAsB,CAAA,EAC5D,kBAAC,QAAD;aAAM,WAAU;uBACb,EACC,KAAgB,KAAK,GAAa,mBAAmB,KAAK,MAC1D,EAAE,uBAAuB,GAAG,CAC7B;aACI,CAAA,CACH;;WAEJ,EAAA,CAAA;UAEN,kBAAC,UAAD;WACE,MAAK;WACL,SAAS;WACT,UAAU,CAAC,KAAe,EAAO;WACjC,WAAW,EACT,8GACA,KAAe,CAAC,EAAO,YACnB,mFACA,sDACL;qBAEA,EAAO,YAAY,gBAAgB;WAC7B,CAAA;UACL;;QAGP,MAAiB,aAChB,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,GAAD,EAAc,WAAU,mDAAoD,CAAA;UAC5E,kBAAC,KAAD;WAAG,WAAU;qBAAwC;WAA0B,CAAA;UAC/E,kBAAC,KAAD;WAAG,WAAU;qBAAmC;WAAkC,CAAA;UAC9E;;QAGP,CAAC,KAAa,CAAC,KAAc,KAC5B,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,GAAD,EAAQ,WAAU,oBAAqB,CAAA;;UAClB,EAAY;UAAO;UAEpC;;QAEE;;MACR,CAAA;KACF,CAAA,CACF;;GACK"}
1
+ {"version":3,"file":"InvestmentDetailsPage.js","names":[],"sources":["../../../../src/kadaikodi/pages/investor/InvestmentDetailsPage.tsx"],"sourcesContent":["/**\n * Investment Details Page\n * @module kadaikodi/pages/investor\n *\n * Serves both /investor/opportunity/:id and /investor/investments/:id. Resolves\n * the opportunity (and the investor's holding, if any) from the live backend by\n * route id. Business metrics are read from the real service-provider record\n * (useProvider by cartId) — never fabricated. The invest panel is wired to the\n * live useInvest mutation; the projected-returns curve is a transparent forecast\n * computed from the stated expected return, not invented performance data.\n */\n\nimport { useCallback, useMemo, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport {\n ArrowLeft,\n TrendingUp,\n Shield,\n Users,\n IndianRupee,\n Star,\n MessageSquare,\n Receipt,\n Tag,\n CheckCircle2,\n Lock,\n Target,\n} from 'lucide-react';\nimport { EmphasisPanel, GlassCard, PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { EmptyState } from '../../components/EmptyState';\nimport { AreaChart, ProgressRing, StatCard } from '../../components/viz';\nimport { useKadaikodi } from '../../context/KadaikodiContext';\nimport {\n useInvestmentOpportunities,\n useInvestments,\n useInvest,\n useProvider,\n} from '../../hooks/useKadaikodiApi';\nimport { cn } from '../../utils/cn';\nimport { formatCurrency, formatDate } from '../../utils/formatters';\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction getRiskConfig(risk: string): { label: string; color: string; badge: string } {\n switch (risk) {\n case 'low':\n return {\n label: 'Low risk',\n color: 'text-status-success-text',\n badge: 'border-status-success-border bg-status-success-bg-subtle text-status-success-text',\n };\n case 'medium':\n return {\n label: 'Medium risk',\n color: 'text-status-warning-text',\n badge: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n };\n case 'high':\n return {\n label: 'High risk',\n color: 'text-status-error-text',\n badge: 'border-status-error-border bg-status-error-bg-subtle text-status-error-text',\n };\n default:\n return { label: risk, color: 'text-text-secondary', badge: 'border-border-default' };\n }\n}\n\n/** Compact rupee formatter for chart axes / tooltips. */\nfunction formatRupeeCompact(value: number): string {\n return formatCurrency(value, { minimumFractionDigits: 0, notation: 'compact' });\n}\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\nexport function InvestmentDetailsPage() {\n const { navigateTo, currentUser } = useKadaikodi();\n const { id } = useParams<{ id: string }>();\n\n const { data: opportunities = [], isLoading: opportunitiesLoading } =\n useInvestmentOpportunities();\n const { data: investments = [], isLoading: investmentsLoading } = useInvestments(\n currentUser?.id || ''\n );\n\n const isLoading = opportunitiesLoading || investmentsLoading;\n\n // The route id may be an investment id or an opportunity id. Resolve the\n // holding first (if it belongs to this investor), then the opportunity.\n const investment = useMemo(() => investments.find((inv) => inv.id === id), [investments, id]);\n const opportunity = useMemo(\n () =>\n opportunities.find((opp) => opp.id === id || opp.id === investment?.opportunityId) ?? null,\n [opportunities, id, investment]\n );\n\n // Real business metrics come from the underlying service-provider record.\n const { data: provider } = useProvider(opportunity?.cartId);\n\n // Invest panel state (wired to the live mutation) ------------------------\n const invest = useInvest();\n const [amount, setAmount] = useState('');\n const [investStatus, setInvestStatus] = useState<'idle' | 'success' | 'error'>('idle');\n\n const minInvestment = opportunity?.minimumInvestment ?? 0;\n const remaining = opportunity\n ? Math.max(0, opportunity.targetAmount - opportunity.raisedAmount)\n : 0;\n const maxInvestment = Math.max(minInvestment, remaining);\n const parsedAmount = Number.parseInt(amount, 10);\n const amountValid =\n !Number.isNaN(parsedAmount) && parsedAmount >= minInvestment && parsedAmount <= maxInvestment;\n\n const handleInvest = useCallback(() => {\n if (!opportunity || !amountValid) {\n setInvestStatus('error');\n return;\n }\n invest.mutate(\n { amount: parsedAmount, investorId: currentUser?.id ?? '', opportunityId: opportunity.id },\n {\n onSuccess: () => {\n setInvestStatus('success');\n window.setTimeout(() => navigateTo('/investor/portfolio'), 1400);\n },\n onError: () => setInvestStatus('error'),\n }\n );\n }, [opportunity, amountValid, invest, parsedAmount, currentUser?.id, navigateTo]);\n\n // Projected-returns forecast: a transparent linear glide from the amount put\n // in today to amount × (1 + expected return) at the end of the lock-in. It is\n // a computed projection from the stated rate, clearly labelled — not data.\n const projection = useMemo(() => {\n if (!opportunity) return [];\n const base = amountValid ? parsedAmount : minInvestment || opportunity.targetAmount / 10;\n const finalValue = base * (1 + opportunity.expectedReturns / 100);\n const months = Math.min(Math.max(opportunity.investmentPeriod, 3), 12);\n const steps = 6;\n return Array.from({ length: steps + 1 }, (_, i) => {\n const monthLabel =\n i === 0 ? 'Now' : i === steps ? `${months}mo` : `M${Math.round((months * i) / steps)}`;\n return { label: monthLabel, value: base + (finalValue - base) * (i / steps) };\n });\n }, [opportunity, amountValid, parsedAmount, minInvestment]);\n\n if (isLoading) {\n return (\n <PageLayout title=\"Investment Details\" description=\"Loading...\">\n <div className=\"space-y-4\">\n <div className=\"h-28 animate-pulse rounded-[var(--radius-card)] bg-bg-sunken\" />\n <div className=\"grid gap-6 lg:grid-cols-3\">\n <div className=\"h-96 animate-pulse rounded-[var(--radius-card)] bg-bg-sunken lg:col-span-2\" />\n <div className=\"h-96 animate-pulse rounded-[var(--radius-card)] bg-bg-sunken\" />\n </div>\n </div>\n </PageLayout>\n );\n }\n\n if (!opportunity && !investment) {\n return (\n <PageLayout title=\"Investment Not Found\">\n <EmptyState\n illustration=\"empty-generic\"\n title=\"Investment not found\"\n description=\"We couldn't find this investment. It may have been closed or the link is out of date.\"\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/investor/portfolio')}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-accent transition-colors\"\n >\n Back to Portfolio\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n const cartName = opportunity?.cartName ?? investment?.cartName ?? 'Investment';\n const risk = opportunity ? getRiskConfig(opportunity.riskLevel) : null;\n const fundingPct =\n opportunity && opportunity.targetAmount > 0\n ? Math.min((opportunity.raisedAmount / opportunity.targetAmount) * 100, 100)\n : 0;\n const daysLeft = opportunity?.expiresAt\n ? Math.max(0, Math.ceil((new Date(opportunity.expiresAt).getTime() - Date.now()) / 86400000))\n : null;\n\n const holdingReturns = investment ? investment.currentValue - investment.investedAmount : 0;\n const holdingReturnsPct =\n investment && investment.investedAmount > 0\n ? (holdingReturns / investment.investedAmount) * 100\n : 0;\n\n const canInvest = Boolean(opportunity) && opportunity?.status === 'active';\n\n return (\n <PageLayout\n title={cartName}\n description=\"Investment details & performance\"\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/investor/portfolio')}\n className=\"flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-accent transition-colors\"\n >\n <ArrowLeft className=\"h-4 w-4\" />\n <span className=\"hidden sm:inline\">Back to Portfolio</span>\n <span className=\"sm:hidden\">Back</span>\n </button>\n }\n >\n <PagePurpose className=\"mb-6\">\n A deep dive into this investment. Review the business behind it, funding progress, expected\n returns and terms on offer, and — if you already hold a stake — exactly what you&apos;ve put\n in and what it&apos;s worth today.\n </PagePurpose>\n\n {/* Existing holding summary — the one emphasis zone when you own a stake */}\n {investment && (\n <EmphasisPanel>\n <h2 className=\"mb-4 text-base font-semibold text-text-primary\">Your Position</h2>\n <div className=\"grid grid-cols-2 gap-4 lg:grid-cols-4\">\n <StatCard\n label=\"Your Investment\"\n value={formatCurrency(investment.investedAmount, { minimumFractionDigits: 0 })}\n icon={<IndianRupee className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Current Value\"\n value={formatCurrency(investment.currentValue, { minimumFractionDigits: 0 })}\n icon={<TrendingUp className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Returns\"\n value={`${holdingReturnsPct >= 0 ? '+' : ''}${holdingReturnsPct.toFixed(1)}%`}\n delta={Number(holdingReturnsPct.toFixed(1))}\n deltaLabel=\"on invested\"\n icon={<TrendingUp className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Realised Returns\"\n value={formatCurrency(investment.returns, { minimumFractionDigits: 0 })}\n icon={<Receipt className=\"h-4 w-4\" />}\n />\n </div>\n </EmphasisPanel>\n )}\n\n <div className=\"mt-6 grid gap-6 lg:grid-cols-3\">\n {/* Left — the business & funding story */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* Hero header with risk badge */}\n {opportunity && (\n <GlassCard treatment=\"glass\" className=\"p-5 sm:p-6\">\n <div className=\"flex flex-wrap items-start justify-between gap-3\">\n <div className=\"min-w-0\">\n <p className=\"text-sm text-text-secondary\">{opportunity.businessOwner}</p>\n <h2 className=\"mt-0.5 text-xl font-bold tracking-tight text-text-primary sm:text-2xl\">\n {opportunity.cartName}\n </h2>\n </div>\n {risk && (\n <span\n className={cn(\n 'inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-sm font-semibold',\n risk.badge\n )}\n >\n <Shield className=\"h-4 w-4\" />\n {risk.label}\n </span>\n )}\n </div>\n {opportunity.description && (\n <p className=\"mt-4 text-sm leading-relaxed text-text-secondary\">\n {opportunity.description}\n </p>\n )}\n </GlassCard>\n )}\n\n {/* Business Metrics — real provider figures */}\n {provider && (\n <section>\n <h3 className=\"mb-3 text-lg font-semibold text-text-primary\">Business Metrics</h3>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-4 sm:gap-4\">\n <StatCard\n label=\"Rating\"\n value={provider.rating > 0 ? provider.rating.toFixed(1) : '—'}\n icon={<Star className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Reviews\"\n value={provider.totalRatings.toLocaleString('en-IN')}\n icon={<MessageSquare className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Avg Order\"\n value={\n provider.averageOrderValue\n ? formatCurrency(provider.averageOrderValue, { minimumFractionDigits: 0 })\n : '—'\n }\n icon={<Receipt className=\"h-4 w-4\" />}\n />\n <StatCard\n label=\"Category\"\n value={\n <span className=\"text-lg font-semibold capitalize\">\n {provider.category || '—'}\n </span>\n }\n icon={<Tag className=\"h-4 w-4\" />}\n />\n </div>\n </section>\n )}\n\n {/* Funding progress with a ProgressRing */}\n {opportunity && (\n <GlassCard treatment=\"plain\" className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 text-lg font-semibold text-text-primary\">Funding Progress</h3>\n <div className=\"flex flex-col items-center gap-6 sm:flex-row sm:gap-8\">\n <ProgressRing\n value={fundingPct}\n size={140}\n label={`${fundingPct.toFixed(0)}%`}\n caption=\"funded\"\n />\n <div className=\"w-full flex-1 space-y-4\">\n <div>\n <div className=\"flex items-baseline justify-between\">\n <span className=\"text-2xl font-bold text-text-primary\">\n {formatCurrency(opportunity.raisedAmount, { minimumFractionDigits: 0 })}\n </span>\n <span className=\"text-sm text-text-secondary\">\n of {formatCurrency(opportunity.targetAmount, { minimumFractionDigits: 0 })}\n </span>\n </div>\n <p className=\"mt-1 text-sm text-text-secondary\">raised so far</p>\n </div>\n <div className=\"grid grid-cols-3 gap-3 border-t border-border-subtle pt-4 text-center\">\n <div>\n <p className=\"text-base font-bold text-text-primary sm:text-lg\">\n {opportunity.investors.length}\n </p>\n <p className=\"text-xs text-text-secondary\">Investors</p>\n </div>\n <div>\n <p className=\"text-base font-bold text-text-primary sm:text-lg\">\n {formatCurrency(remaining, {\n minimumFractionDigits: 0,\n notation: 'compact',\n })}\n </p>\n <p className=\"text-xs text-text-secondary\">To Go</p>\n </div>\n <div>\n <p className=\"text-base font-bold text-text-primary sm:text-lg\">\n {daysLeft !== null ? `${daysLeft}` : '—'}\n </p>\n <p className=\"text-xs text-text-secondary\">Days Left</p>\n </div>\n </div>\n </div>\n </div>\n </GlassCard>\n )}\n\n {/* Your holding detail */}\n {investment && (\n <GlassCard treatment=\"plain\" className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 text-lg font-semibold text-text-primary\">Your Holding</h3>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <div>\n <p className=\"text-sm text-text-secondary\">Invested Amount</p>\n <p className=\"font-medium text-text-primary\">\n {formatCurrency(investment.investedAmount, { minimumFractionDigits: 0 })}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Current Value</p>\n <p className=\"font-medium text-text-primary\">\n {formatCurrency(investment.currentValue, { minimumFractionDigits: 0 })}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Returns Realised</p>\n <p className=\"font-medium text-text-primary\">\n {formatCurrency(investment.returns, { minimumFractionDigits: 0 })}\n </p>\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">Invested On</p>\n <p className=\"font-medium text-text-primary\">\n {formatDate(investment.investmentDate, { hour: undefined, minute: undefined })}\n </p>\n </div>\n </div>\n </GlassCard>\n )}\n </div>\n\n {/* Right — sticky invest panel */}\n <div className=\"space-y-6\">\n <div className=\"lg:sticky lg:top-6\">\n <GlassCard treatment=\"glass\" glow className=\"p-5 sm:p-6\">\n <h3 className=\"mb-4 text-lg font-semibold text-text-primary\">\n {canInvest ? 'Invest' : 'Investment Terms'}\n </h3>\n\n {/* Terms */}\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between\">\n <span className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <TrendingUp className=\"h-4 w-4\" />\n Expected returns\n </span>\n <span className=\"font-semibold text-status-success-text\">\n {opportunity?.expectedReturns ?? investment?.returnsPercentage}% p.a.\n </span>\n </div>\n <div className=\"flex items-center justify-between\">\n <span className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Lock className=\"h-4 w-4\" />\n Lock-in\n </span>\n <span className=\"font-semibold text-text-primary\">\n {opportunity?.investmentPeriod ?? '—'} months\n </span>\n </div>\n {opportunity && (\n <div className=\"flex items-center justify-between\">\n <span className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <IndianRupee className=\"h-4 w-4\" />\n Minimum\n </span>\n <span className=\"font-semibold text-text-primary\">\n {formatCurrency(opportunity.minimumInvestment, { minimumFractionDigits: 0 })}\n </span>\n </div>\n )}\n {opportunity && opportunity.investors.length > 0 && (\n <div className=\"flex items-center justify-between\">\n <span className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Users className=\"h-4 w-4\" />\n Investors\n </span>\n <span className=\"font-semibold text-text-primary\">\n {opportunity.investors.length}\n </span>\n </div>\n )}\n </div>\n\n {canInvest && investStatus !== 'success' && (\n <div className=\"mt-5 space-y-4 border-t border-border-subtle pt-5\">\n <div>\n <label\n htmlFor=\"invest-amount\"\n className=\"mb-1.5 block text-sm font-medium text-text-primary\"\n >\n Amount\n </label>\n <div className=\"relative\">\n <IndianRupee className=\"absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-text-secondary\" />\n <input\n id=\"invest-amount\"\n type=\"number\"\n inputMode=\"numeric\"\n value={amount}\n onChange={(e) => {\n setAmount(e.target.value);\n setInvestStatus('idle');\n }}\n placeholder={String(minInvestment)}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface py-2.5 pl-9 pr-3 text-base font-semibold text-text-primary placeholder:font-normal placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]\"\n />\n </div>\n <p className=\"mt-1.5 text-xs text-text-secondary\">\n {formatCurrency(minInvestment, { minimumFractionDigits: 0 })} –{' '}\n {formatCurrency(maxInvestment, { minimumFractionDigits: 0 })}\n </p>\n {investStatus === 'error' && (\n <p className=\"mt-1 text-xs text-status-error-text\">\n Enter an amount between{' '}\n {formatCurrency(minInvestment, { minimumFractionDigits: 0 })} and{' '}\n {formatCurrency(maxInvestment, { minimumFractionDigits: 0 })}.\n </p>\n )}\n </div>\n\n {/* Projected returns forecast */}\n <div>\n <p className=\"mb-1 text-sm font-medium text-text-primary\">Projected returns</p>\n <p className=\"mb-2 text-xs text-text-muted\">\n Forecast at {opportunity?.expectedReturns}% over{' '}\n {opportunity?.investmentPeriod} months\n </p>\n <AreaChart\n series={projection}\n height={140}\n valueFormatter={formatRupeeCompact}\n title=\"Projected value over the lock-in period\"\n />\n {amountValid && (\n <div className=\"mt-2 flex items-center justify-between rounded-lg bg-bg-sunken/60 px-3 py-2 text-sm\">\n <span className=\"text-text-secondary\">Projected value</span>\n <span className=\"font-semibold text-status-success-text\">\n {formatCurrency(\n parsedAmount * (1 + (opportunity?.expectedReturns ?? 0) / 100),\n { minimumFractionDigits: 0 }\n )}\n </span>\n </div>\n )}\n </div>\n\n <button\n type=\"button\"\n onClick={handleInvest}\n disabled={!amountValid || invest.isPending}\n className={cn(\n 'flex w-full items-center justify-center gap-2 rounded-lg px-4 py-3 text-sm font-semibold transition-colors',\n amountValid && !invest.isPending\n ? 'bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover'\n : 'cursor-not-allowed bg-bg-sunken text-text-secondary'\n )}\n >\n {invest.isPending ? 'Processing…' : 'Invest Now'}\n </button>\n </div>\n )}\n\n {investStatus === 'success' && (\n <div className=\"mt-5 border-t border-border-subtle pt-5 text-center\">\n <CheckCircle2 className=\"mx-auto mb-2 h-10 w-10 text-status-success-text\" />\n <p className=\"text-sm font-medium text-text-primary\">Investment successful!</p>\n <p className=\"mt-1 text-xs text-text-secondary\">Redirecting to your portfolio…</p>\n </div>\n )}\n\n {!canInvest && !investment && opportunity && (\n <div className=\"mt-5 flex items-center gap-2 rounded-lg border border-border-subtle bg-bg-sunken/50 p-3 text-sm text-text-secondary\">\n <Target className=\"h-4 w-4 shrink-0\" />\n This opportunity is {opportunity.status}. It&apos;s no longer accepting\n investments.\n </div>\n )}\n </GlassCard>\n </div>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"InvestmentOpportunitiesPage.d.ts","sourceRoot":"","sources":["../../../../src/kadaikodi/pages/investor/InvestmentOpportunitiesPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAqKH,wBAAgB,2BAA2B,gCAqnB1C"}
1
+ {"version":3,"file":"InvestmentOpportunitiesPage.d.ts","sourceRoot":"","sources":["../../../../src/kadaikodi/pages/investor/InvestmentOpportunitiesPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAqKH,wBAAgB,2BAA2B,gCAinB1C"}