@burdenoff/microfe-housingvista 2026.625.2 → 2026.626.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/housingvista/pages/amc/AMCManagementPage.js +109 -82
- package/dist/housingvista/pages/amc/AMCManagementPage.js.map +1 -1
- package/dist/housingvista/pages/assets/AssetManagementPage.js +57 -31
- package/dist/housingvista/pages/assets/AssetManagementPage.js.map +1 -1
- package/dist/housingvista/pages/finance/HomeLoansPage.js +190 -162
- package/dist/housingvista/pages/finance/HomeLoansPage.js.map +1 -1
- package/dist/housingvista/pages/investments/InvestmentsPage.js +57 -31
- package/dist/housingvista/pages/investments/InvestmentsPage.js.map +1 -1
- package/dist/housingvista/pages/legal/LegalPage.js +36 -13
- package/dist/housingvista/pages/legal/LegalPage.js.map +1 -1
- package/dist/housingvista/pages/marketplace/MarketplacePage.js +36 -11
- package/dist/housingvista/pages/marketplace/MarketplacePage.js.map +1 -1
- package/dist/housingvista/pages/transactions/TransactionsPage.js +95 -69
- package/dist/housingvista/pages/transactions/TransactionsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AssetManagementPage.js","names":[],"sources":["../../../../src/housingvista/pages/assets/AssetManagementPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Building,\n DollarSign,\n TrendingUp,\n BarChart3,\n Plus,\n Download,\n ArrowUpRight,\n ArrowDownRight,\n MapPin,\n Eye,\n} from 'lucide-react';\nimport { EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components';\nimport { StatCard } from '../../components';\nimport { DataTable, type Column } from '../../components';\nimport { StatusBadge } from '../../components';\nimport { FilterBar } from '../../components';\nimport { EmptyState } from '../../components';\nimport { useAssets } from '../../hooks';\nimport { useServerPagination } from '../../hooks/useServerPagination';\nimport { cn, formatCurrency } from '../../utils/helpers';\nimport type { Asset } from '../../types';\n\nfunction getStatusVariant(status: Asset['status']): 'success' | 'warning' | 'info' | 'muted' {\n switch (status) {\n case 'active':\n return 'success';\n case 'under_renovation':\n return 'warning';\n case 'pending_sale':\n return 'info';\n case 'sold':\n return 'muted';\n default:\n return 'muted';\n }\n}\n\nexport function AssetManagementPage() {\n const navigate = useNavigate();\n const pagination = useServerPagination(10);\n const { data: assets, loading } = useAssets(pagination.pagination);\n const [search, setSearch] = useState('');\n\n const filtered = useMemo(() => {\n if (!search) return assets;\n const q = search.toLowerCase();\n return assets.filter(\n (a) =>\n a.title.toLowerCase().includes(q) || a.location.toLowerCase().includes(q) || a.type.toLowerCase().includes(q)\n );\n }, [assets, search]);\n\n const portfolioStats = useMemo(() => {\n const totalValue = assets.reduce((sum, a) => sum + a.currentValue, 0);\n const totalMonthlyIncome = assets\n .filter((a) => a.rentalYield)\n .reduce((sum, a) => sum + (a.currentValue * (a.rentalYield || 0)) / 100 / 12, 0);\n const avgAppreciation =\n assets.length > 0 ? assets.reduce((sum, a) => sum + a.appreciationPercent, 0) / assets.length : 0;\n const totalPurchasePrice = assets.reduce((sum, a) => sum + a.purchasePrice, 0);\n const totalROI = totalPurchasePrice > 0 ? ((totalValue - totalPurchasePrice) / totalPurchasePrice) * 100 : 0;\n return { totalValue, totalMonthlyIncome, avgAppreciation, totalROI };\n }, [assets]);\n\n const assetColumns: Column<Asset>[] = [\n {\n key: 'title',\n header: 'Property',\n render: (item) => (\n <div className=\"flex items-center gap-3\">\n <img src={item.imageUrl} alt={item.title} className=\"h-10 w-10 rounded-md object-cover\" />\n <div>\n <div className=\"font-medium text-text-primary\">{item.title}</div>\n <div className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n {item.location}\n </div>\n </div>\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n render: (item) => <span className=\"text-sm capitalize text-text-primary\">{item.type}</span>,\n },\n {\n key: 'purchasePrice',\n header: 'Purchase Price',\n render: (item) => <span className=\"text-sm text-text-primary\">{formatCurrency(item.purchasePrice)}</span>,\n },\n {\n key: 'currentValue',\n header: 'Current Value',\n render: (item) => (\n <span className=\"text-sm font-medium text-text-primary\">{formatCurrency(item.currentValue)}</span>\n ),\n },\n {\n key: 'appreciationPercent',\n header: 'Change',\n render: (item) => {\n const change = item.currentValue - item.purchasePrice;\n const isPositive = change >= 0;\n return (\n <div\n className={cn(\n 'flex items-center gap-1 text-sm font-medium',\n isPositive ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'\n )}\n >\n {isPositive ? <ArrowUpRight className=\"h-3 w-3\" /> : <ArrowDownRight className=\"h-3 w-3\" />}\n {formatCurrency(Math.abs(change))}\n <span className=\"text-xs\">({item.appreciationPercent}%)</span>\n </div>\n );\n },\n },\n {\n key: 'status',\n header: 'Status',\n render: (item) => <StatusBadge label={item.status.replace('_', ' ')} variant={getStatusVariant(item.status)} />,\n },\n {\n key: 'actions',\n header: '',\n sortable: false,\n render: (item) => (\n <button\n onClick={(e) => {\n e.stopPropagation();\n navigate(`/assets/${item.id}`);\n }}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Eye className=\"h-3 w-3\" />\n View\n </button>\n ),\n },\n ];\n\n return (\n <PageLayout\n title=\"Asset Management\"\n description=\"Monitor and manage your property portfolio performance\"\n purpose=\"See how every property you own is performing as an investment — current value, appreciation, rental yield, and overall ROI in one portfolio view. Use it to decide what to hold, renovate, or sell.\"\n actions={\n <div className=\"flex items-center gap-2\">\n <button className=\"inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\">\n <Download className=\"h-4 w-4\" />\n Export\n </button>\n <button className=\"inline-flex items-center gap-2 rounded-md bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors hover:bg-foreground/90\">\n <Plus className=\"h-4 w-4\" />\n Add Asset\n </button>\n </div>\n }\n >\n {/* Portfolio Value Stats — primary insight zone (one EmphasisPanel per page) */}\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n label=\"Total Value\"\n value={portfolioStats.totalValue}\n prefix=\"$\"\n icon={<DollarSign className=\"h-4 w-4\" />}\n trend=\"up\"\n change={8}\n />\n <StatCard\n label=\"Monthly Income\"\n value={Math.round(portfolioStats.totalMonthlyIncome)}\n prefix=\"$\"\n icon={<TrendingUp className=\"h-4 w-4\" />}\n trend=\"up\"\n change={5}\n />\n <StatCard\n label=\"Avg. Appreciation\"\n value={`${portfolioStats.avgAppreciation.toFixed(1)}%`}\n icon={<BarChart3 className=\"h-4 w-4\" />}\n trend=\"up\"\n change={2}\n />\n <StatCard\n label=\"Overall ROI\"\n value={`${portfolioStats.totalROI.toFixed(1)}%`}\n icon={<Building className=\"h-4 w-4\" />}\n trend=\"up\"\n change={3}\n />\n </div>\n </EmphasisPanel>\n\n {/* Asset Cards Overview */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {assets.map((asset) => {\n const change = asset.currentValue - asset.purchasePrice;\n const isPositive = change >= 0;\n return (\n <div\n key={asset.id}\n className=\"cursor-pointer rounded-lg border border-border-subtle bg-bg-surface p-4 shadow-sm transition-shadow hover:shadow-md\"\n onClick={() => navigate(`/assets/${asset.id}`)}\n >\n <div className=\"mb-3 flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n <img src={asset.imageUrl} alt={asset.title} className=\"h-12 w-12 rounded-lg object-cover\" />\n <div>\n <h3 className=\"font-medium text-text-primary\">{asset.title}</h3>\n <p className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n {asset.location}\n </p>\n </div>\n </div>\n <StatusBadge label={asset.status.replace('_', ' ')} variant={getStatusVariant(asset.status)} />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <p className=\"text-xs text-text-secondary\">Current Value</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(asset.currentValue)}</p>\n </div>\n <div>\n <p className=\"text-xs text-text-secondary\">Change</p>\n <p\n className={cn(\n 'flex items-center gap-0.5 text-sm font-semibold',\n isPositive ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'\n )}\n >\n {isPositive ? <ArrowUpRight className=\"h-3 w-3\" /> : <ArrowDownRight className=\"h-3 w-3\" />}\n {asset.appreciationPercent}%\n </p>\n </div>\n {asset.rentalYield && (\n <div>\n <p className=\"text-xs text-text-secondary\">Rental Yield</p>\n <p className=\"text-sm font-semibold text-text-primary\">{asset.rentalYield}%</p>\n </div>\n )}\n <div>\n <p className=\"text-xs text-text-secondary\">Category</p>\n <p className=\"text-sm capitalize text-text-primary\">{asset.category}</p>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n\n {/* Asset Table */}\n <div>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">All Assets</h2>\n <FilterBar\n searchValue={search}\n onSearchChange={setSearch}\n searchPlaceholder=\"Search assets by name, location, or type...\"\n />\n <div className=\"mt-4\">\n {filtered.length > 0 ? (\n <DataTable\n columns={assetColumns}\n data={filtered}\n keyField=\"id\"\n onRowClick={(item) => navigate(`/assets/${item.id}`)}\n pageSize={10}\n serverPagination={{\n page: pagination.page,\n pageSize: pagination.pageSize,\n hasNextPage: assets.length >= pagination.pageSize,\n loading,\n onPageChange: pagination.setPage,\n onPageSizeChange: pagination.setPageSize,\n }}\n />\n ) : (\n <EmptyState\n illustration=\"empty-search\"\n icon={<Building className=\"h-12 w-12\" />}\n title=\"No assets found\"\n description=\"Try adjusting your search criteria or add a new asset.\"\n action={\n <button className=\"inline-flex items-center gap-2 rounded-md bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors hover:bg-foreground/90\">\n <Plus className=\"h-4 w-4\" />\n Add Asset\n </button>\n }\n />\n )}\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AssetManagementPage.js","names":[],"sources":["../../../../src/housingvista/pages/assets/AssetManagementPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Building,\n DollarSign,\n TrendingUp,\n BarChart3,\n Plus,\n Download,\n ArrowUpRight,\n ArrowDownRight,\n MapPin,\n Eye,\n} from 'lucide-react';\nimport { EmphasisPanel, NextSteps } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components';\nimport { StatCard } from '../../components';\nimport { DataTable, type Column } from '../../components';\nimport { StatusBadge } from '../../components';\nimport { FilterBar } from '../../components';\nimport { EmptyState } from '../../components';\nimport { useAssets } from '../../hooks';\nimport { useServerPagination } from '../../hooks/useServerPagination';\nimport { cn, formatCurrency } from '../../utils/helpers';\nimport type { Asset } from '../../types';\n\nfunction getStatusVariant(status: Asset['status']): 'success' | 'warning' | 'info' | 'muted' {\n switch (status) {\n case 'active':\n return 'success';\n case 'under_renovation':\n return 'warning';\n case 'pending_sale':\n return 'info';\n case 'sold':\n return 'muted';\n default:\n return 'muted';\n }\n}\n\nexport function AssetManagementPage() {\n const navigate = useNavigate();\n const pagination = useServerPagination(10);\n const { data: assets, loading } = useAssets(pagination.pagination);\n const [search, setSearch] = useState('');\n\n const filtered = useMemo(() => {\n if (!search) return assets;\n const q = search.toLowerCase();\n return assets.filter(\n (a) =>\n a.title.toLowerCase().includes(q) || a.location.toLowerCase().includes(q) || a.type.toLowerCase().includes(q)\n );\n }, [assets, search]);\n\n const portfolioStats = useMemo(() => {\n const totalValue = assets.reduce((sum, a) => sum + a.currentValue, 0);\n const totalMonthlyIncome = assets\n .filter((a) => a.rentalYield)\n .reduce((sum, a) => sum + (a.currentValue * (a.rentalYield || 0)) / 100 / 12, 0);\n const avgAppreciation =\n assets.length > 0 ? assets.reduce((sum, a) => sum + a.appreciationPercent, 0) / assets.length : 0;\n const totalPurchasePrice = assets.reduce((sum, a) => sum + a.purchasePrice, 0);\n const totalROI = totalPurchasePrice > 0 ? ((totalValue - totalPurchasePrice) / totalPurchasePrice) * 100 : 0;\n return { totalValue, totalMonthlyIncome, avgAppreciation, totalROI };\n }, [assets]);\n\n const assetColumns: Column<Asset>[] = [\n {\n key: 'title',\n header: 'Property',\n render: (item) => (\n <div className=\"flex items-center gap-3\">\n <img src={item.imageUrl} alt={item.title} className=\"h-10 w-10 rounded-md object-cover\" />\n <div>\n <div className=\"font-medium text-text-primary\">{item.title}</div>\n <div className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n {item.location}\n </div>\n </div>\n </div>\n ),\n },\n {\n key: 'type',\n header: 'Type',\n render: (item) => <span className=\"text-sm capitalize text-text-primary\">{item.type}</span>,\n },\n {\n key: 'purchasePrice',\n header: 'Purchase Price',\n render: (item) => <span className=\"text-sm text-text-primary\">{formatCurrency(item.purchasePrice)}</span>,\n },\n {\n key: 'currentValue',\n header: 'Current Value',\n render: (item) => (\n <span className=\"text-sm font-medium text-text-primary\">{formatCurrency(item.currentValue)}</span>\n ),\n },\n {\n key: 'appreciationPercent',\n header: 'Change',\n render: (item) => {\n const change = item.currentValue - item.purchasePrice;\n const isPositive = change >= 0;\n return (\n <div\n className={cn(\n 'flex items-center gap-1 text-sm font-medium',\n isPositive ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'\n )}\n >\n {isPositive ? <ArrowUpRight className=\"h-3 w-3\" /> : <ArrowDownRight className=\"h-3 w-3\" />}\n {formatCurrency(Math.abs(change))}\n <span className=\"text-xs\">({item.appreciationPercent}%)</span>\n </div>\n );\n },\n },\n {\n key: 'status',\n header: 'Status',\n render: (item) => <StatusBadge label={item.status.replace('_', ' ')} variant={getStatusVariant(item.status)} />,\n },\n {\n key: 'actions',\n header: '',\n sortable: false,\n render: (item) => (\n <button\n onClick={(e) => {\n e.stopPropagation();\n navigate(`/assets/${item.id}`);\n }}\n className=\"inline-flex items-center gap-1 rounded-md border border-border-subtle px-2 py-1 text-xs font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Eye className=\"h-3 w-3\" />\n View\n </button>\n ),\n },\n ];\n\n return (\n <PageLayout\n title=\"Asset Management\"\n description=\"Monitor and manage your property portfolio performance\"\n purpose=\"See how every property you own is performing as an investment — current value, appreciation, rental yield, and overall ROI in one portfolio view. Use it to decide what to hold, renovate, or sell.\"\n actions={\n <div className=\"flex items-center gap-2\">\n <button className=\"inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\">\n <Download className=\"h-4 w-4\" />\n Export\n </button>\n <button className=\"inline-flex items-center gap-2 rounded-md bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors hover:bg-foreground/90\">\n <Plus className=\"h-4 w-4\" />\n Add Asset\n </button>\n </div>\n }\n >\n {/* Portfolio Value Stats — primary insight zone (one EmphasisPanel per page) */}\n <EmphasisPanel className=\"p-4\">\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4\">\n <StatCard\n label=\"Total Value\"\n value={portfolioStats.totalValue}\n prefix=\"$\"\n icon={<DollarSign className=\"h-4 w-4\" />}\n trend=\"up\"\n change={8}\n />\n <StatCard\n label=\"Monthly Income\"\n value={Math.round(portfolioStats.totalMonthlyIncome)}\n prefix=\"$\"\n icon={<TrendingUp className=\"h-4 w-4\" />}\n trend=\"up\"\n change={5}\n />\n <StatCard\n label=\"Avg. Appreciation\"\n value={`${portfolioStats.avgAppreciation.toFixed(1)}%`}\n icon={<BarChart3 className=\"h-4 w-4\" />}\n trend=\"up\"\n change={2}\n />\n <StatCard\n label=\"Overall ROI\"\n value={`${portfolioStats.totalROI.toFixed(1)}%`}\n icon={<Building className=\"h-4 w-4\" />}\n trend=\"up\"\n change={3}\n />\n </div>\n </EmphasisPanel>\n\n <NextSteps\n storageKey=\"housingvista-assets\"\n steps={[\n {\n id: 'open-asset',\n label: 'Open an asset to see full performance',\n description: 'Drill into value history, yield, and ROI for any property.',\n onClick: () => {\n const first = assets[0];\n if (first) navigate(`/assets/${first.id}`);\n },\n },\n {\n id: 'compare-market',\n label: 'Compare against market trends',\n description: 'Check market analytics to benchmark your portfolio.',\n onClick: () => navigate('/market-analytics'),\n },\n {\n id: 'add-asset',\n label: 'Add a property you already own',\n description: 'Bring an existing holding into your portfolio view.',\n onClick: () => navigate('/properties/new'),\n },\n ]}\n />\n\n {/* Asset Cards Overview */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {assets.map((asset) => {\n const change = asset.currentValue - asset.purchasePrice;\n const isPositive = change >= 0;\n return (\n <div\n key={asset.id}\n className=\"cursor-pointer rounded-lg border border-border-subtle bg-bg-surface p-4 shadow-sm transition-shadow hover:shadow-md\"\n onClick={() => navigate(`/assets/${asset.id}`)}\n >\n <div className=\"mb-3 flex items-start justify-between\">\n <div className=\"flex items-center gap-3\">\n <img src={asset.imageUrl} alt={asset.title} className=\"h-12 w-12 rounded-lg object-cover\" />\n <div>\n <h3 className=\"font-medium text-text-primary\">{asset.title}</h3>\n <p className=\"flex items-center gap-1 text-xs text-text-secondary\">\n <MapPin className=\"h-3 w-3\" />\n {asset.location}\n </p>\n </div>\n </div>\n <StatusBadge label={asset.status.replace('_', ' ')} variant={getStatusVariant(asset.status)} />\n </div>\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <p className=\"text-xs text-text-secondary\">Current Value</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(asset.currentValue)}</p>\n </div>\n <div>\n <p className=\"text-xs text-text-secondary\">Change</p>\n <p\n className={cn(\n 'flex items-center gap-0.5 text-sm font-semibold',\n isPositive ? 'text-emerald-600 dark:text-emerald-400' : 'text-red-600 dark:text-red-400'\n )}\n >\n {isPositive ? <ArrowUpRight className=\"h-3 w-3\" /> : <ArrowDownRight className=\"h-3 w-3\" />}\n {asset.appreciationPercent}%\n </p>\n </div>\n {asset.rentalYield && (\n <div>\n <p className=\"text-xs text-text-secondary\">Rental Yield</p>\n <p className=\"text-sm font-semibold text-text-primary\">{asset.rentalYield}%</p>\n </div>\n )}\n <div>\n <p className=\"text-xs text-text-secondary\">Category</p>\n <p className=\"text-sm capitalize text-text-primary\">{asset.category}</p>\n </div>\n </div>\n </div>\n );\n })}\n </div>\n\n {/* Asset Table */}\n <div>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">All Assets</h2>\n <FilterBar\n searchValue={search}\n onSearchChange={setSearch}\n searchPlaceholder=\"Search assets by name, location, or type...\"\n />\n <div className=\"mt-4\">\n {filtered.length > 0 ? (\n <DataTable\n columns={assetColumns}\n data={filtered}\n keyField=\"id\"\n onRowClick={(item) => navigate(`/assets/${item.id}`)}\n pageSize={10}\n serverPagination={{\n page: pagination.page,\n pageSize: pagination.pageSize,\n hasNextPage: assets.length >= pagination.pageSize,\n loading,\n onPageChange: pagination.setPage,\n onPageSizeChange: pagination.setPageSize,\n }}\n />\n ) : (\n <EmptyState\n illustration=\"empty-search\"\n icon={<Building className=\"h-12 w-12\" />}\n title=\"No assets found\"\n description=\"Try adjusting your search criteria or add a new asset.\"\n action={\n <button className=\"inline-flex items-center gap-2 rounded-md bg-foreground px-4 py-2 text-sm font-medium text-background transition-colors hover:bg-foreground/90\">\n <Plus className=\"h-4 w-4\" />\n Add Asset\n </button>\n }\n />\n )}\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -7,20 +7,20 @@ import { useMemo as a, useState as o } from "react";
|
|
|
7
7
|
import { ArrowRight as s, Building2 as c, Calculator as l, CheckCircle as u, Gift as d, Star as f } from "lucide-react";
|
|
8
8
|
import { useNavigate as p } from "react-router-dom";
|
|
9
9
|
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
10
|
-
import { EmphasisPanel as g } from "@burdenoff/fe-libs/ui";
|
|
10
|
+
import { EmphasisPanel as g, NextSteps as _ } from "@burdenoff/fe-libs/ui";
|
|
11
11
|
//#region src/housingvista/pages/finance/HomeLoansPage.tsx
|
|
12
|
-
function
|
|
12
|
+
function v(e, t, n) {
|
|
13
13
|
if (e <= 0 || t <= 0 || n <= 0) return 0;
|
|
14
14
|
let r = t / 12 / 100, i = n * 12, a = e * r * (1 + r) ** +i / ((1 + r) ** +i - 1);
|
|
15
15
|
return Math.round(a);
|
|
16
16
|
}
|
|
17
|
-
function
|
|
17
|
+
function y({ rating: t }) {
|
|
18
18
|
return /* @__PURE__ */ m("div", {
|
|
19
19
|
className: "flex items-center gap-0.5",
|
|
20
20
|
children: Array.from({ length: 5 }).map((n, r) => /* @__PURE__ */ m(f, { className: e("h-3 w-3", r < Math.floor(t) ? "fill-amber-400 text-amber-400" : "text-text-secondary/30") }, r))
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
|
-
function
|
|
23
|
+
function b({ loan: n }) {
|
|
24
24
|
let r = p();
|
|
25
25
|
return /* @__PURE__ */ h("div", {
|
|
26
26
|
className: "rounded-lg border border-border-subtle bg-bg-surface p-5 shadow-sm transition-shadow hover:shadow-md",
|
|
@@ -37,7 +37,7 @@ function y({ loan: n }) {
|
|
|
37
37
|
children: n.bankName
|
|
38
38
|
}), /* @__PURE__ */ h("div", {
|
|
39
39
|
className: "flex items-center gap-1.5 mt-0.5",
|
|
40
|
-
children: [/* @__PURE__ */ m(
|
|
40
|
+
children: [/* @__PURE__ */ m(y, { rating: n.rating }), /* @__PURE__ */ m("span", {
|
|
41
41
|
className: "text-xs text-text-secondary",
|
|
42
42
|
children: n.rating
|
|
43
43
|
})]
|
|
@@ -131,179 +131,207 @@ function y({ loan: n }) {
|
|
|
131
131
|
]
|
|
132
132
|
});
|
|
133
133
|
}
|
|
134
|
-
function
|
|
135
|
-
p()
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
]), S = x * d * 12, C = S - s;
|
|
134
|
+
function x() {
|
|
135
|
+
let e = p(), { data: s } = i(), [u, d] = o(5e5), [f, y] = o(20), [x, S] = o(6.75), C = a(() => v(u, x, f), [
|
|
136
|
+
u,
|
|
137
|
+
x,
|
|
138
|
+
f
|
|
139
|
+
]), w = C * f * 12, T = w - u;
|
|
141
140
|
return /* @__PURE__ */ h(n, {
|
|
142
141
|
title: "Home Loans",
|
|
143
142
|
description: "Compare rates, calculate EMI, and apply for home loans",
|
|
144
143
|
purpose: "Work out what a home loan will really cost before you commit. Use the EMI calculator to model your monthly payment and total interest, then compare offers from partner banks and apply to the one that fits.",
|
|
145
|
-
children: [
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
className: "mb-4 flex items-center gap-2",
|
|
149
|
-
children: [/* @__PURE__ */ m(l, { className: "h-5 w-5 text-text-secondary" }), /* @__PURE__ */ m("h2", {
|
|
150
|
-
className: "text-lg font-semibold text-text-primary",
|
|
151
|
-
children: "EMI Calculator"
|
|
152
|
-
})]
|
|
153
|
-
}), /* @__PURE__ */ h("div", {
|
|
154
|
-
className: "grid gap-6 md:grid-cols-2",
|
|
144
|
+
children: [
|
|
145
|
+
/* @__PURE__ */ h(g, {
|
|
146
|
+
className: "p-5",
|
|
155
147
|
children: [/* @__PURE__ */ h("div", {
|
|
156
|
-
className: "
|
|
157
|
-
children: [
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
148
|
+
className: "mb-4 flex items-center gap-2",
|
|
149
|
+
children: [/* @__PURE__ */ m(l, { className: "h-5 w-5 text-text-secondary" }), /* @__PURE__ */ m("h2", {
|
|
150
|
+
className: "text-lg font-semibold text-text-primary",
|
|
151
|
+
children: "EMI Calculator"
|
|
152
|
+
})]
|
|
153
|
+
}), /* @__PURE__ */ h("div", {
|
|
154
|
+
className: "grid gap-6 md:grid-cols-2",
|
|
155
|
+
children: [/* @__PURE__ */ h("div", {
|
|
156
|
+
className: "space-y-5",
|
|
157
|
+
children: [
|
|
158
|
+
/* @__PURE__ */ h("div", { children: [
|
|
159
|
+
/* @__PURE__ */ h("div", {
|
|
160
|
+
className: "flex items-center justify-between mb-2",
|
|
161
|
+
children: [/* @__PURE__ */ m("label", {
|
|
162
|
+
className: "text-sm font-medium text-text-primary",
|
|
163
|
+
children: "Loan Amount"
|
|
164
|
+
}), /* @__PURE__ */ m("span", {
|
|
165
|
+
className: "text-sm font-semibold text-text-primary",
|
|
166
|
+
children: t(u)
|
|
167
|
+
})]
|
|
168
|
+
}),
|
|
169
|
+
/* @__PURE__ */ m("input", {
|
|
170
|
+
type: "range",
|
|
171
|
+
min: 5e4,
|
|
172
|
+
max: 5e6,
|
|
173
|
+
step: 1e4,
|
|
174
|
+
value: u,
|
|
175
|
+
onChange: (e) => d(Number(e.target.value)),
|
|
176
|
+
className: "w-full accent-foreground"
|
|
177
|
+
}),
|
|
178
|
+
/* @__PURE__ */ h("div", {
|
|
179
|
+
className: "flex justify-between text-xs text-text-secondary mt-1",
|
|
180
|
+
children: [/* @__PURE__ */ m("span", { children: "$50K" }), /* @__PURE__ */ m("span", { children: "$5M" })]
|
|
181
|
+
})
|
|
182
|
+
] }),
|
|
183
|
+
/* @__PURE__ */ h("div", { children: [
|
|
184
|
+
/* @__PURE__ */ h("div", {
|
|
185
|
+
className: "flex items-center justify-between mb-2",
|
|
186
|
+
children: [/* @__PURE__ */ m("label", {
|
|
187
|
+
className: "text-sm font-medium text-text-primary",
|
|
188
|
+
children: "Tenure (years)"
|
|
189
|
+
}), /* @__PURE__ */ h("span", {
|
|
190
|
+
className: "text-sm font-semibold text-text-primary",
|
|
191
|
+
children: [f, " years"]
|
|
192
|
+
})]
|
|
193
|
+
}),
|
|
194
|
+
/* @__PURE__ */ m("input", {
|
|
195
|
+
type: "range",
|
|
196
|
+
min: 1,
|
|
197
|
+
max: 30,
|
|
198
|
+
step: 1,
|
|
199
|
+
value: f,
|
|
200
|
+
onChange: (e) => y(Number(e.target.value)),
|
|
201
|
+
className: "w-full accent-foreground"
|
|
202
|
+
}),
|
|
203
|
+
/* @__PURE__ */ h("div", {
|
|
204
|
+
className: "flex justify-between text-xs text-text-secondary mt-1",
|
|
205
|
+
children: [/* @__PURE__ */ m("span", { children: "1 yr" }), /* @__PURE__ */ m("span", { children: "30 yrs" })]
|
|
206
|
+
})
|
|
207
|
+
] }),
|
|
208
|
+
/* @__PURE__ */ h("div", { children: [
|
|
209
|
+
/* @__PURE__ */ h("div", {
|
|
210
|
+
className: "flex items-center justify-between mb-2",
|
|
211
|
+
children: [/* @__PURE__ */ m("label", {
|
|
212
|
+
className: "text-sm font-medium text-text-primary",
|
|
213
|
+
children: "Interest Rate (%)"
|
|
214
|
+
}), /* @__PURE__ */ h("span", {
|
|
215
|
+
className: "text-sm font-semibold text-text-primary",
|
|
216
|
+
children: [x, "%"]
|
|
217
|
+
})]
|
|
218
|
+
}),
|
|
219
|
+
/* @__PURE__ */ m("input", {
|
|
220
|
+
type: "range",
|
|
221
|
+
min: 3,
|
|
222
|
+
max: 15,
|
|
223
|
+
step: .05,
|
|
224
|
+
value: x,
|
|
225
|
+
onChange: (e) => S(Number(e.target.value)),
|
|
226
|
+
className: "w-full accent-foreground"
|
|
227
|
+
}),
|
|
228
|
+
/* @__PURE__ */ h("div", {
|
|
229
|
+
className: "flex justify-between text-xs text-text-secondary mt-1",
|
|
230
|
+
children: [/* @__PURE__ */ m("span", { children: "3%" }), /* @__PURE__ */ m("span", { children: "15%" })]
|
|
231
|
+
})
|
|
232
|
+
] })
|
|
233
|
+
]
|
|
234
|
+
}), /* @__PURE__ */ h("div", {
|
|
235
|
+
className: "flex flex-col items-center justify-center rounded-lg bg-bg-sunken/50 p-6",
|
|
236
|
+
children: [
|
|
237
|
+
/* @__PURE__ */ m("p", {
|
|
238
|
+
className: "text-sm text-text-secondary",
|
|
239
|
+
children: "Monthly EMI"
|
|
168
240
|
}),
|
|
169
|
-
/* @__PURE__ */ m("
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
max: 5e6,
|
|
173
|
-
step: 1e4,
|
|
174
|
-
value: s,
|
|
175
|
-
onChange: (e) => u(Number(e.target.value)),
|
|
176
|
-
className: "w-full accent-foreground"
|
|
241
|
+
/* @__PURE__ */ m("p", {
|
|
242
|
+
className: "my-2 text-4xl font-bold text-text-primary",
|
|
243
|
+
children: t(C)
|
|
177
244
|
}),
|
|
178
245
|
/* @__PURE__ */ h("div", {
|
|
179
|
-
className: "
|
|
180
|
-
children: [/* @__PURE__ */
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}), /* @__PURE__ */ h("
|
|
190
|
-
className: "text-
|
|
191
|
-
children: [
|
|
246
|
+
className: "mt-4 grid w-full grid-cols-2 gap-4",
|
|
247
|
+
children: [/* @__PURE__ */ h("div", {
|
|
248
|
+
className: "text-center",
|
|
249
|
+
children: [/* @__PURE__ */ m("p", {
|
|
250
|
+
className: "text-xs text-text-secondary",
|
|
251
|
+
children: "Total Payment"
|
|
252
|
+
}), /* @__PURE__ */ m("p", {
|
|
253
|
+
className: "text-sm font-semibold text-text-primary",
|
|
254
|
+
children: t(w)
|
|
255
|
+
})]
|
|
256
|
+
}), /* @__PURE__ */ h("div", {
|
|
257
|
+
className: "text-center",
|
|
258
|
+
children: [/* @__PURE__ */ m("p", {
|
|
259
|
+
className: "text-xs text-text-secondary",
|
|
260
|
+
children: "Total Interest"
|
|
261
|
+
}), /* @__PURE__ */ m("p", {
|
|
262
|
+
className: "text-sm font-semibold text-red-600 dark:text-red-400",
|
|
263
|
+
children: t(T)
|
|
264
|
+
})]
|
|
192
265
|
})]
|
|
193
266
|
}),
|
|
194
|
-
/* @__PURE__ */ m("input", {
|
|
195
|
-
type: "range",
|
|
196
|
-
min: 1,
|
|
197
|
-
max: 30,
|
|
198
|
-
step: 1,
|
|
199
|
-
value: d,
|
|
200
|
-
onChange: (e) => f(Number(e.target.value)),
|
|
201
|
-
className: "w-full accent-foreground"
|
|
202
|
-
}),
|
|
203
267
|
/* @__PURE__ */ h("div", {
|
|
204
|
-
className: "
|
|
205
|
-
children: [/* @__PURE__ */
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
}), /* @__PURE__ */ h("
|
|
215
|
-
className: "
|
|
216
|
-
children: [
|
|
268
|
+
className: "mt-4 w-full",
|
|
269
|
+
children: [/* @__PURE__ */ h("div", {
|
|
270
|
+
className: "flex h-3 w-full overflow-hidden rounded-full",
|
|
271
|
+
children: [/* @__PURE__ */ m("div", {
|
|
272
|
+
className: "bg-foreground transition-all",
|
|
273
|
+
style: { width: `${u / w * 100}%` }
|
|
274
|
+
}), /* @__PURE__ */ m("div", {
|
|
275
|
+
className: "bg-red-400 dark:bg-red-600 transition-all",
|
|
276
|
+
style: { width: `${T / w * 100}%` }
|
|
277
|
+
})]
|
|
278
|
+
}), /* @__PURE__ */ h("div", {
|
|
279
|
+
className: "mt-1.5 flex justify-between text-xs text-text-secondary",
|
|
280
|
+
children: [/* @__PURE__ */ h("span", {
|
|
281
|
+
className: "flex items-center gap-1",
|
|
282
|
+
children: [/* @__PURE__ */ m("span", { className: "h-2 w-2 rounded-full bg-foreground" }), " Principal"]
|
|
283
|
+
}), /* @__PURE__ */ h("span", {
|
|
284
|
+
className: "flex items-center gap-1",
|
|
285
|
+
children: [/* @__PURE__ */ m("span", { className: "h-2 w-2 rounded-full bg-red-400 dark:bg-red-600" }), " Interest"]
|
|
286
|
+
})]
|
|
217
287
|
})]
|
|
218
|
-
}),
|
|
219
|
-
/* @__PURE__ */ m("input", {
|
|
220
|
-
type: "range",
|
|
221
|
-
min: 3,
|
|
222
|
-
max: 15,
|
|
223
|
-
step: .05,
|
|
224
|
-
value: v,
|
|
225
|
-
onChange: (e) => b(Number(e.target.value)),
|
|
226
|
-
className: "w-full accent-foreground"
|
|
227
|
-
}),
|
|
228
|
-
/* @__PURE__ */ h("div", {
|
|
229
|
-
className: "flex justify-between text-xs text-text-secondary mt-1",
|
|
230
|
-
children: [/* @__PURE__ */ m("span", { children: "3%" }), /* @__PURE__ */ m("span", { children: "15%" })]
|
|
231
288
|
})
|
|
232
|
-
]
|
|
233
|
-
]
|
|
234
|
-
}), /* @__PURE__ */ h("div", {
|
|
235
|
-
className: "flex flex-col items-center justify-center rounded-lg bg-bg-sunken/50 p-6",
|
|
236
|
-
children: [
|
|
237
|
-
/* @__PURE__ */ m("p", {
|
|
238
|
-
className: "text-sm text-text-secondary",
|
|
239
|
-
children: "Monthly EMI"
|
|
240
|
-
}),
|
|
241
|
-
/* @__PURE__ */ m("p", {
|
|
242
|
-
className: "my-2 text-4xl font-bold text-text-primary",
|
|
243
|
-
children: t(x)
|
|
244
|
-
}),
|
|
245
|
-
/* @__PURE__ */ h("div", {
|
|
246
|
-
className: "mt-4 grid w-full grid-cols-2 gap-4",
|
|
247
|
-
children: [/* @__PURE__ */ h("div", {
|
|
248
|
-
className: "text-center",
|
|
249
|
-
children: [/* @__PURE__ */ m("p", {
|
|
250
|
-
className: "text-xs text-text-secondary",
|
|
251
|
-
children: "Total Payment"
|
|
252
|
-
}), /* @__PURE__ */ m("p", {
|
|
253
|
-
className: "text-sm font-semibold text-text-primary",
|
|
254
|
-
children: t(S)
|
|
255
|
-
})]
|
|
256
|
-
}), /* @__PURE__ */ h("div", {
|
|
257
|
-
className: "text-center",
|
|
258
|
-
children: [/* @__PURE__ */ m("p", {
|
|
259
|
-
className: "text-xs text-text-secondary",
|
|
260
|
-
children: "Total Interest"
|
|
261
|
-
}), /* @__PURE__ */ m("p", {
|
|
262
|
-
className: "text-sm font-semibold text-red-600 dark:text-red-400",
|
|
263
|
-
children: t(C)
|
|
264
|
-
})]
|
|
265
|
-
})]
|
|
266
|
-
}),
|
|
267
|
-
/* @__PURE__ */ h("div", {
|
|
268
|
-
className: "mt-4 w-full",
|
|
269
|
-
children: [/* @__PURE__ */ h("div", {
|
|
270
|
-
className: "flex h-3 w-full overflow-hidden rounded-full",
|
|
271
|
-
children: [/* @__PURE__ */ m("div", {
|
|
272
|
-
className: "bg-foreground transition-all",
|
|
273
|
-
style: { width: `${s / S * 100}%` }
|
|
274
|
-
}), /* @__PURE__ */ m("div", {
|
|
275
|
-
className: "bg-red-400 dark:bg-red-600 transition-all",
|
|
276
|
-
style: { width: `${C / S * 100}%` }
|
|
277
|
-
})]
|
|
278
|
-
}), /* @__PURE__ */ h("div", {
|
|
279
|
-
className: "mt-1.5 flex justify-between text-xs text-text-secondary",
|
|
280
|
-
children: [/* @__PURE__ */ h("span", {
|
|
281
|
-
className: "flex items-center gap-1",
|
|
282
|
-
children: [/* @__PURE__ */ m("span", { className: "h-2 w-2 rounded-full bg-foreground" }), " Principal"]
|
|
283
|
-
}), /* @__PURE__ */ h("span", {
|
|
284
|
-
className: "flex items-center gap-1",
|
|
285
|
-
children: [/* @__PURE__ */ m("span", { className: "h-2 w-2 rounded-full bg-red-400 dark:bg-red-600" }), " Interest"]
|
|
286
|
-
})]
|
|
287
|
-
})]
|
|
288
|
-
})
|
|
289
|
-
]
|
|
289
|
+
]
|
|
290
|
+
})]
|
|
290
291
|
})]
|
|
291
|
-
})
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
292
|
+
}),
|
|
293
|
+
/* @__PURE__ */ m(_, {
|
|
294
|
+
storageKey: "housingvista-home-loans",
|
|
295
|
+
steps: [
|
|
296
|
+
{
|
|
297
|
+
id: "apply-best",
|
|
298
|
+
label: "Apply to the best-rated offer",
|
|
299
|
+
description: "Start an application with a top partner bank.",
|
|
300
|
+
onClick: () => {
|
|
301
|
+
let t = [...s].sort((e, t) => t.rating - e.rating)[0];
|
|
302
|
+
t && e(`/home-loans/${t.id}/apply`);
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: "browse-properties",
|
|
307
|
+
label: "Find a property to finance",
|
|
308
|
+
description: "Browse listings that fit the amount you just modeled.",
|
|
309
|
+
onClick: () => e("/properties")
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: "track-transactions",
|
|
313
|
+
label: "Track your purchase",
|
|
314
|
+
description: "See ongoing deals where this loan would apply.",
|
|
315
|
+
onClick: () => e("/transactions")
|
|
316
|
+
}
|
|
317
|
+
]
|
|
318
|
+
}),
|
|
319
|
+
/* @__PURE__ */ h("div", { children: [/* @__PURE__ */ m("h2", {
|
|
320
|
+
className: "mb-4 text-lg font-semibold text-text-primary",
|
|
321
|
+
children: "Available Loan Offers"
|
|
322
|
+
}), s.length > 0 ? /* @__PURE__ */ m("div", {
|
|
323
|
+
className: "grid gap-4 sm:grid-cols-2 lg:grid-cols-3",
|
|
324
|
+
children: s.map((e) => /* @__PURE__ */ m(b, { loan: e }, e.id))
|
|
325
|
+
}) : /* @__PURE__ */ m(r, {
|
|
326
|
+
illustration: "empty-data",
|
|
327
|
+
icon: /* @__PURE__ */ m(c, { className: "h-12 w-12" }),
|
|
328
|
+
title: "No loan offers available",
|
|
329
|
+
description: "Check back later for new loan offers from our partner banks."
|
|
330
|
+
})] })
|
|
331
|
+
]
|
|
304
332
|
});
|
|
305
333
|
}
|
|
306
334
|
//#endregion
|
|
307
|
-
export {
|
|
335
|
+
export { x as HomeLoansPage };
|
|
308
336
|
|
|
309
337
|
//# sourceMappingURL=HomeLoansPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HomeLoansPage.js","names":[],"sources":["../../../../src/housingvista/pages/finance/HomeLoansPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Building2,\n Calculator,\n Star,\n CheckCircle,\n TrendingDown,\n Percent,\n DollarSign,\n Clock,\n ArrowRight,\n Gift,\n} from 'lucide-react';\nimport { EmphasisPanel } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState } from '../../components';\nimport { useHomeLoans } from '../../hooks';\nimport { cn, formatCurrency, formatNumber } from '../../utils/helpers';\nimport type { HomeLoan } from '../../types';\n\nfunction calculateEMI(principal: number, annualRate: number, tenureYears: number): number {\n if (principal <= 0 || annualRate <= 0 || tenureYears <= 0) return 0;\n const monthlyRate = annualRate / 12 / 100;\n const months = tenureYears * 12;\n const emi = (principal * monthlyRate * Math.pow(1 + monthlyRate, months)) / (Math.pow(1 + monthlyRate, months) - 1);\n return Math.round(emi);\n}\n\nfunction RatingStars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5\">\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'h-3 w-3',\n i < Math.floor(rating) ? 'fill-amber-400 text-amber-400' : 'text-text-secondary/30'\n )}\n />\n ))}\n </div>\n );\n}\n\nfunction LoanCard({ loan }: { loan: HomeLoan }) {\n const navigate = useNavigate();\n\n return (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-5 shadow-sm transition-shadow hover:shadow-md\">\n {/* Header */}\n <div className=\"mb-4 flex items-start justify-between gap-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-bg-sunken\">\n <Building2 className=\"h-6 w-6 text-text-secondary\" />\n </div>\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">{loan.bankName}</h3>\n <div className=\"flex items-center gap-1.5 mt-0.5\">\n <RatingStars rating={loan.rating} />\n <span className=\"text-xs text-text-secondary\">{loan.rating}</span>\n </div>\n </div>\n </div>\n {loan.specialOffer && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400\">\n <Gift className=\"h-3 w-3\" />\n Offer\n </span>\n )}\n </div>\n\n {/* Key Stats Grid */}\n <div className=\"mb-4 grid grid-cols-2 gap-3\">\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">Interest Rate</p>\n <p className=\"text-lg font-bold text-text-primary\">{loan.interestRate}%</p>\n </div>\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">EMI / Lakh</p>\n <p className=\"text-lg font-bold text-text-primary\">${loan.emiPerLakh}</p>\n </div>\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">Max Tenure</p>\n <p className=\"text-sm font-semibold text-text-primary\">{loan.maxTenure} years</p>\n </div>\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">Max Amount</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(loan.maxAmount)}</p>\n </div>\n </div>\n\n {/* Processing Fee */}\n <div className=\"mb-3 flex items-center justify-between text-xs\">\n <span className=\"text-text-secondary\">Processing Fee</span>\n <span className=\"font-medium text-text-primary\">{loan.processingFee}</span>\n </div>\n\n {/* Prepayment Charges */}\n <div className=\"mb-3 flex items-center justify-between text-xs\">\n <span className=\"text-text-secondary\">Prepayment Charges</span>\n <span\n className={cn(\n 'font-medium',\n loan.prepaymentCharges === 'None' ? 'text-emerald-600 dark:text-emerald-400' : 'text-text-primary'\n )}\n >\n {loan.prepaymentCharges}\n </span>\n </div>\n\n {/* Special Offer */}\n {loan.specialOffer && (\n <div className=\"mb-3 rounded-md bg-amber-50 p-2.5 text-xs text-amber-800 dark:bg-amber-900/20 dark:text-amber-300\">\n {loan.specialOffer}\n </div>\n )}\n\n {/* Features */}\n <div className=\"mb-4 space-y-1.5\">\n {loan.features.map((feature, i) => (\n <div key={i} className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <CheckCircle className=\"h-3 w-3 shrink-0 text-emerald-500\" />\n {feature}\n </div>\n ))}\n </div>\n\n {/* Apply Button */}\n <button\n onClick={() => navigate(`/home-loans/${loan.id}/apply`)}\n className=\"flex w-full items-center justify-center gap-1.5 rounded-md bg-foreground px-4 py-2.5 text-xs font-medium text-background transition-colors hover:bg-foreground/90\"\n >\n Apply Now\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n );\n}\n\nexport function HomeLoansPage() {\n const navigate = useNavigate();\n const { data: loans } = useHomeLoans();\n\n // EMI Calculator state\n const [loanAmount, setLoanAmount] = useState(500000);\n const [tenure, setTenure] = useState(20);\n const [interestRate, setInterestRate] = useState(6.75);\n\n const emi = useMemo(() => calculateEMI(loanAmount, interestRate, tenure), [loanAmount, interestRate, tenure]);\n const totalPayment = emi * tenure * 12;\n const totalInterest = totalPayment - loanAmount;\n\n return (\n <PageLayout\n title=\"Home Loans\"\n description=\"Compare rates, calculate EMI, and apply for home loans\"\n purpose=\"Work out what a home loan will really cost before you commit. Use the EMI calculator to model your monthly payment and total interest, then compare offers from partner banks and apply to the one that fits.\"\n >\n {/* EMI Calculator — primary emphasis zone (one per page) */}\n <EmphasisPanel className=\"p-5\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Calculator className=\"h-5 w-5 text-text-secondary\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">EMI Calculator</h2>\n </div>\n\n <div className=\"grid gap-6 md:grid-cols-2\">\n {/* Inputs */}\n <div className=\"space-y-5\">\n {/* Loan Amount */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <label className=\"text-sm font-medium text-text-primary\">Loan Amount</label>\n <span className=\"text-sm font-semibold text-text-primary\">{formatCurrency(loanAmount)}</span>\n </div>\n <input\n type=\"range\"\n min={50000}\n max={5000000}\n step={10000}\n value={loanAmount}\n onChange={(e) => setLoanAmount(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"flex justify-between text-xs text-text-secondary mt-1\">\n <span>$50K</span>\n <span>$5M</span>\n </div>\n </div>\n\n {/* Tenure */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <label className=\"text-sm font-medium text-text-primary\">Tenure (years)</label>\n <span className=\"text-sm font-semibold text-text-primary\">{tenure} years</span>\n </div>\n <input\n type=\"range\"\n min={1}\n max={30}\n step={1}\n value={tenure}\n onChange={(e) => setTenure(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"flex justify-between text-xs text-text-secondary mt-1\">\n <span>1 yr</span>\n <span>30 yrs</span>\n </div>\n </div>\n\n {/* Interest Rate */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <label className=\"text-sm font-medium text-text-primary\">Interest Rate (%)</label>\n <span className=\"text-sm font-semibold text-text-primary\">{interestRate}%</span>\n </div>\n <input\n type=\"range\"\n min={3}\n max={15}\n step={0.05}\n value={interestRate}\n onChange={(e) => setInterestRate(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"flex justify-between text-xs text-text-secondary mt-1\">\n <span>3%</span>\n <span>15%</span>\n </div>\n </div>\n </div>\n\n {/* Results */}\n <div className=\"flex flex-col items-center justify-center rounded-lg bg-bg-sunken/50 p-6\">\n <p className=\"text-sm text-text-secondary\">Monthly EMI</p>\n <p className=\"my-2 text-4xl font-bold text-text-primary\">{formatCurrency(emi)}</p>\n <div className=\"mt-4 grid w-full grid-cols-2 gap-4\">\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Payment</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(totalPayment)}</p>\n </div>\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Interest</p>\n <p className=\"text-sm font-semibold text-red-600 dark:text-red-400\">{formatCurrency(totalInterest)}</p>\n </div>\n </div>\n {/* Visual breakdown bar */}\n <div className=\"mt-4 w-full\">\n <div className=\"flex h-3 w-full overflow-hidden rounded-full\">\n <div\n className=\"bg-foreground transition-all\"\n style={{ width: `${(loanAmount / totalPayment) * 100}%` }}\n />\n <div\n className=\"bg-red-400 dark:bg-red-600 transition-all\"\n style={{ width: `${(totalInterest / totalPayment) * 100}%` }}\n />\n </div>\n <div className=\"mt-1.5 flex justify-between text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded-full bg-foreground\" /> Principal\n </span>\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded-full bg-red-400 dark:bg-red-600\" /> Interest\n </span>\n </div>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n {/* Loan Offers */}\n <div>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Available Loan Offers</h2>\n {loans.length > 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {loans.map((loan) => (\n <LoanCard key={loan.id} loan={loan} />\n ))}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Building2 className=\"h-12 w-12\" />}\n title=\"No loan offers available\"\n description=\"Check back later for new loan offers from our partner banks.\"\n />\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HomeLoansPage.js","names":[],"sources":["../../../../src/housingvista/pages/finance/HomeLoansPage.tsx"],"sourcesContent":["import { useState, useMemo } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport {\n Building2,\n Calculator,\n Star,\n CheckCircle,\n TrendingDown,\n Percent,\n DollarSign,\n Clock,\n ArrowRight,\n Gift,\n} from 'lucide-react';\nimport { EmphasisPanel, NextSteps } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState } from '../../components';\nimport { useHomeLoans } from '../../hooks';\nimport { cn, formatCurrency, formatNumber } from '../../utils/helpers';\nimport type { HomeLoan } from '../../types';\n\nfunction calculateEMI(principal: number, annualRate: number, tenureYears: number): number {\n if (principal <= 0 || annualRate <= 0 || tenureYears <= 0) return 0;\n const monthlyRate = annualRate / 12 / 100;\n const months = tenureYears * 12;\n const emi = (principal * monthlyRate * Math.pow(1 + monthlyRate, months)) / (Math.pow(1 + monthlyRate, months) - 1);\n return Math.round(emi);\n}\n\nfunction RatingStars({ rating }: { rating: number }) {\n return (\n <div className=\"flex items-center gap-0.5\">\n {Array.from({ length: 5 }).map((_, i) => (\n <Star\n key={i}\n className={cn(\n 'h-3 w-3',\n i < Math.floor(rating) ? 'fill-amber-400 text-amber-400' : 'text-text-secondary/30'\n )}\n />\n ))}\n </div>\n );\n}\n\nfunction LoanCard({ loan }: { loan: HomeLoan }) {\n const navigate = useNavigate();\n\n return (\n <div className=\"rounded-lg border border-border-subtle bg-bg-surface p-5 shadow-sm transition-shadow hover:shadow-md\">\n {/* Header */}\n <div className=\"mb-4 flex items-start justify-between gap-3\">\n <div className=\"flex items-center gap-3\">\n <div className=\"flex h-12 w-12 shrink-0 items-center justify-center rounded-lg bg-bg-sunken\">\n <Building2 className=\"h-6 w-6 text-text-secondary\" />\n </div>\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">{loan.bankName}</h3>\n <div className=\"flex items-center gap-1.5 mt-0.5\">\n <RatingStars rating={loan.rating} />\n <span className=\"text-xs text-text-secondary\">{loan.rating}</span>\n </div>\n </div>\n </div>\n {loan.specialOffer && (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-400\">\n <Gift className=\"h-3 w-3\" />\n Offer\n </span>\n )}\n </div>\n\n {/* Key Stats Grid */}\n <div className=\"mb-4 grid grid-cols-2 gap-3\">\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">Interest Rate</p>\n <p className=\"text-lg font-bold text-text-primary\">{loan.interestRate}%</p>\n </div>\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">EMI / Lakh</p>\n <p className=\"text-lg font-bold text-text-primary\">${loan.emiPerLakh}</p>\n </div>\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">Max Tenure</p>\n <p className=\"text-sm font-semibold text-text-primary\">{loan.maxTenure} years</p>\n </div>\n <div className=\"rounded-md bg-bg-sunken/50 p-2.5\">\n <p className=\"text-xs text-text-secondary\">Max Amount</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(loan.maxAmount)}</p>\n </div>\n </div>\n\n {/* Processing Fee */}\n <div className=\"mb-3 flex items-center justify-between text-xs\">\n <span className=\"text-text-secondary\">Processing Fee</span>\n <span className=\"font-medium text-text-primary\">{loan.processingFee}</span>\n </div>\n\n {/* Prepayment Charges */}\n <div className=\"mb-3 flex items-center justify-between text-xs\">\n <span className=\"text-text-secondary\">Prepayment Charges</span>\n <span\n className={cn(\n 'font-medium',\n loan.prepaymentCharges === 'None' ? 'text-emerald-600 dark:text-emerald-400' : 'text-text-primary'\n )}\n >\n {loan.prepaymentCharges}\n </span>\n </div>\n\n {/* Special Offer */}\n {loan.specialOffer && (\n <div className=\"mb-3 rounded-md bg-amber-50 p-2.5 text-xs text-amber-800 dark:bg-amber-900/20 dark:text-amber-300\">\n {loan.specialOffer}\n </div>\n )}\n\n {/* Features */}\n <div className=\"mb-4 space-y-1.5\">\n {loan.features.map((feature, i) => (\n <div key={i} className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <CheckCircle className=\"h-3 w-3 shrink-0 text-emerald-500\" />\n {feature}\n </div>\n ))}\n </div>\n\n {/* Apply Button */}\n <button\n onClick={() => navigate(`/home-loans/${loan.id}/apply`)}\n className=\"flex w-full items-center justify-center gap-1.5 rounded-md bg-foreground px-4 py-2.5 text-xs font-medium text-background transition-colors hover:bg-foreground/90\"\n >\n Apply Now\n <ArrowRight className=\"h-3.5 w-3.5\" />\n </button>\n </div>\n );\n}\n\nexport function HomeLoansPage() {\n const navigate = useNavigate();\n const { data: loans } = useHomeLoans();\n\n // EMI Calculator state\n const [loanAmount, setLoanAmount] = useState(500000);\n const [tenure, setTenure] = useState(20);\n const [interestRate, setInterestRate] = useState(6.75);\n\n const emi = useMemo(() => calculateEMI(loanAmount, interestRate, tenure), [loanAmount, interestRate, tenure]);\n const totalPayment = emi * tenure * 12;\n const totalInterest = totalPayment - loanAmount;\n\n return (\n <PageLayout\n title=\"Home Loans\"\n description=\"Compare rates, calculate EMI, and apply for home loans\"\n purpose=\"Work out what a home loan will really cost before you commit. Use the EMI calculator to model your monthly payment and total interest, then compare offers from partner banks and apply to the one that fits.\"\n >\n {/* EMI Calculator — primary emphasis zone (one per page) */}\n <EmphasisPanel className=\"p-5\">\n <div className=\"mb-4 flex items-center gap-2\">\n <Calculator className=\"h-5 w-5 text-text-secondary\" />\n <h2 className=\"text-lg font-semibold text-text-primary\">EMI Calculator</h2>\n </div>\n\n <div className=\"grid gap-6 md:grid-cols-2\">\n {/* Inputs */}\n <div className=\"space-y-5\">\n {/* Loan Amount */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <label className=\"text-sm font-medium text-text-primary\">Loan Amount</label>\n <span className=\"text-sm font-semibold text-text-primary\">{formatCurrency(loanAmount)}</span>\n </div>\n <input\n type=\"range\"\n min={50000}\n max={5000000}\n step={10000}\n value={loanAmount}\n onChange={(e) => setLoanAmount(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"flex justify-between text-xs text-text-secondary mt-1\">\n <span>$50K</span>\n <span>$5M</span>\n </div>\n </div>\n\n {/* Tenure */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <label className=\"text-sm font-medium text-text-primary\">Tenure (years)</label>\n <span className=\"text-sm font-semibold text-text-primary\">{tenure} years</span>\n </div>\n <input\n type=\"range\"\n min={1}\n max={30}\n step={1}\n value={tenure}\n onChange={(e) => setTenure(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"flex justify-between text-xs text-text-secondary mt-1\">\n <span>1 yr</span>\n <span>30 yrs</span>\n </div>\n </div>\n\n {/* Interest Rate */}\n <div>\n <div className=\"flex items-center justify-between mb-2\">\n <label className=\"text-sm font-medium text-text-primary\">Interest Rate (%)</label>\n <span className=\"text-sm font-semibold text-text-primary\">{interestRate}%</span>\n </div>\n <input\n type=\"range\"\n min={3}\n max={15}\n step={0.05}\n value={interestRate}\n onChange={(e) => setInterestRate(Number(e.target.value))}\n className=\"w-full accent-foreground\"\n />\n <div className=\"flex justify-between text-xs text-text-secondary mt-1\">\n <span>3%</span>\n <span>15%</span>\n </div>\n </div>\n </div>\n\n {/* Results */}\n <div className=\"flex flex-col items-center justify-center rounded-lg bg-bg-sunken/50 p-6\">\n <p className=\"text-sm text-text-secondary\">Monthly EMI</p>\n <p className=\"my-2 text-4xl font-bold text-text-primary\">{formatCurrency(emi)}</p>\n <div className=\"mt-4 grid w-full grid-cols-2 gap-4\">\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Payment</p>\n <p className=\"text-sm font-semibold text-text-primary\">{formatCurrency(totalPayment)}</p>\n </div>\n <div className=\"text-center\">\n <p className=\"text-xs text-text-secondary\">Total Interest</p>\n <p className=\"text-sm font-semibold text-red-600 dark:text-red-400\">{formatCurrency(totalInterest)}</p>\n </div>\n </div>\n {/* Visual breakdown bar */}\n <div className=\"mt-4 w-full\">\n <div className=\"flex h-3 w-full overflow-hidden rounded-full\">\n <div\n className=\"bg-foreground transition-all\"\n style={{ width: `${(loanAmount / totalPayment) * 100}%` }}\n />\n <div\n className=\"bg-red-400 dark:bg-red-600 transition-all\"\n style={{ width: `${(totalInterest / totalPayment) * 100}%` }}\n />\n </div>\n <div className=\"mt-1.5 flex justify-between text-xs text-text-secondary\">\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded-full bg-foreground\" /> Principal\n </span>\n <span className=\"flex items-center gap-1\">\n <span className=\"h-2 w-2 rounded-full bg-red-400 dark:bg-red-600\" /> Interest\n </span>\n </div>\n </div>\n </div>\n </div>\n </EmphasisPanel>\n\n <NextSteps\n storageKey=\"housingvista-home-loans\"\n steps={[\n {\n id: 'apply-best',\n label: 'Apply to the best-rated offer',\n description: 'Start an application with a top partner bank.',\n onClick: () => {\n const best = [...loans].sort((a, b) => b.rating - a.rating)[0];\n if (best) navigate(`/home-loans/${best.id}/apply`);\n },\n },\n {\n id: 'browse-properties',\n label: 'Find a property to finance',\n description: 'Browse listings that fit the amount you just modeled.',\n onClick: () => navigate('/properties'),\n },\n {\n id: 'track-transactions',\n label: 'Track your purchase',\n description: 'See ongoing deals where this loan would apply.',\n onClick: () => navigate('/transactions'),\n },\n ]}\n />\n\n {/* Loan Offers */}\n <div>\n <h2 className=\"mb-4 text-lg font-semibold text-text-primary\">Available Loan Offers</h2>\n {loans.length > 0 ? (\n <div className=\"grid gap-4 sm:grid-cols-2 lg:grid-cols-3\">\n {loans.map((loan) => (\n <LoanCard key={loan.id} loan={loan} />\n ))}\n </div>\n ) : (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Building2 className=\"h-12 w-12\" />}\n title=\"No loan offers available\"\n description=\"Check back later for new loan offers from our partner banks.\"\n />\n )}\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|