From e3aedbc2f6713c86de4bafd4f3e28bf24d98e6b5 Mon Sep 17 00:00:00 2001 From: Big-cedar Date: Mon, 27 Apr 2026 15:13:20 +0100 Subject: [PATCH] feat: implement comparison history, loading bar, recently viewed & multi-currency --- package-lock.json | 1 + package.json | 2 +- src/app/compare/page.tsx | 406 ++++++++---------------- src/app/page.tsx | 4 + src/components/ClientProviders.tsx | 3 + src/components/LoadingProgressBar.tsx | 96 ++++++ src/components/MultiCurrencyBalance.tsx | 227 +++++++++++++ src/components/PropertyDetail.tsx | 17 +- src/components/RecentlyViewed.tsx | 91 ++++++ src/components/WalletConnector.tsx | 14 +- src/store/comparisonHistoryStore.ts | 66 ++++ src/store/recentlyViewedStore.ts | 62 ++++ 12 files changed, 700 insertions(+), 289 deletions(-) create mode 100644 src/components/LoadingProgressBar.tsx create mode 100644 src/components/MultiCurrencyBalance.tsx create mode 100644 src/components/RecentlyViewed.tsx create mode 100644 src/store/comparisonHistoryStore.ts create mode 100644 src/store/recentlyViewedStore.ts diff --git a/package-lock.json b/package-lock.json index 2a5eb802..0ada9593 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17933,6 +17933,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/package.json b/package.json index 41959922..67b88876 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "test:cy": "cypress run", "test:cy:ui": "cypress open", "test:cy:component": "cypress run --component", - "test:cy:component:ui": "cypress open --component", + "test:cy:component:ui": "cypress open --component" }, "dependencies": { "@coinbase/wallet-sdk": "^4.3.7", diff --git a/src/app/compare/page.tsx b/src/app/compare/page.tsx index 3ce31aa8..0ceaacf2 100644 --- a/src/app/compare/page.tsx +++ b/src/app/compare/page.tsx @@ -3,8 +3,12 @@ import React, { useEffect, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import Link from 'next/link'; -import { ArrowLeft, Share2, Download } from 'lucide-react'; +import { ArrowLeft, Share2, Download, Clock, Trash2, FileText } from 'lucide-react'; import { propertyService } from '@/lib/propertyService'; +import { useComparisonHistoryStore } from '@/store/comparisonHistoryStore'; +import { useComparisonStore } from '@/store/comparisonStore'; +import type { Property } from '@/types/property'; +import { formatPrice, formatROI } from '@/utils/searchUtils'; interface ComparisonMetric { label: string; @@ -24,21 +28,11 @@ const comparisonMetrics: ComparisonMetric[] = [ key: 'location', format: (value: Property['location']) => `${value.city}, ${value.state}`, }, - { - label: 'Property Type', - key: 'propertyType', - format: (value) => PROPERTY_TYPE_LABELS[value as keyof typeof PROPERTY_TYPE_LABELS], - }, - { - label: 'Blockchain', - key: 'blockchain', - format: (value) => BLOCKCHAIN_LABELS[value as keyof typeof BLOCKCHAIN_LABELS], - }, { label: 'Total Price', key: 'price.total', format: (value) => formatPrice(value), - higherIsBetter: false, // Lower price might be better + higherIsBetter: false, }, { label: 'Price per Token', @@ -96,17 +90,14 @@ function getBestValue(properties: Property[], metric: ComparisonMetric): number if (numericValues.length === 0) return null; - if (metric.higherIsBetter) { - return Math.max(...numericValues); - } else { - return Math.min(...numericValues); - } + return metric.higherIsBetter ? Math.max(...numericValues) : Math.min(...numericValues); } export default function ComparePage() { const router = useRouter(); const searchParams = useSearchParams(); const { selectedProperties, clearProperties } = useComparisonStore(); + const { addComparison, history, removeComparison, clearHistory } = useComparisonHistoryStore(); const [properties, setProperties] = useState([]); useEffect(() => { @@ -114,33 +105,30 @@ export default function ComparePage() { const propertyIds = searchParams.get('ids')?.split(',') || []; if (propertyIds.length > 0) { - // Fetch properties by IDs const fetchedProperties: Property[] = []; - for (const id of propertyIds.slice(0, 3)) { // Max 3 + for (const id of propertyIds.slice(0, 3)) { const property = await propertyService.getPropertyById(id); if (property) { fetchedProperties.push(property); } } setProperties(fetchedProperties); + addComparison(propertyIds.slice(0, 3)); } else { - // Use selected properties from store setProperties(selectedProperties); } }; loadProperties(); - }, [searchParams, selectedProperties]); + }, [searchParams, selectedProperties, addComparison]); const handleShare = () => { const propertyIds = properties.map(p => p.id).join(','); const url = `${window.location.origin}/compare?ids=${propertyIds}`; navigator.clipboard.writeText(url); - // You could show a toast notification here }; const handleExport = () => { - // Simple CSV export const headers = comparisonMetrics.map(m => m.label); const rows = properties.map(property => comparisonMetrics.map(metric => @@ -158,6 +146,66 @@ export default function ComparePage() { URL.revokeObjectURL(url); }; + const handleExportPDF = () => { + const printWindow = window.open('', '_blank'); + if (!printWindow) return; + + const html = ` + + + + Property Comparison + + + +

Property Comparison Report

+

Generated on ${new Date().toLocaleDateString()}

+ + + + + ${properties.map(p => ``).join('')} + + + + ${comparisonMetrics.map(metric => ` + + + ${properties.map(p => ``).join('')} + + `).join('')} + +
Metric${p.name}
${metric.label}${metric.format(getNestedValue(p, metric.key), p)}
+ + + `; + + printWindow.document.write(html); + printWindow.document.close(); + printWindow.print(); + }; + + const formatTimestamp = (timestamp: number) => { + const date = new Date(timestamp); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + return `${diffDays}d ago`; + }; + if (properties.length === 0) { return (
@@ -213,6 +261,13 @@ export default function ComparePage() { Export CSV +
@@ -225,7 +280,7 @@ export default function ComparePage() { Metric - {properties.map((property, index) => ( + {properties.map((property) => (
{property.name} -
- {BLOCKCHAIN_LABELS[property.blockchain]} -
))} @@ -257,7 +306,7 @@ export default function ComparePage() { {properties.map((property) => { const value = getNestedValue(property, metric.key); - const isBest = bestValue !== null && value === bestValue && metric.higherIsBetter; + const isBest = bestValue !== null && value === bestValue; return ( @@ -294,256 +343,63 @@ export default function ComparePage() { Browse More Properties - - - ); -} -/home/semicolon/Documents/Drip/PropChain-FrontEnd/src/app/compare/page.tsx -import { useEffect, useMemo, useState } from 'react'; -import Image from 'next/image'; -import Link from 'next/link'; -import { useSearchParams } from 'next/navigation'; -import { propertyService } from '@/lib/propertyService'; -import type { Property } from '@/types/property'; -import { formatPrice, formatROI } from '@/utils/searchUtils'; -import { ArrowLeft, ExternalLink } from 'lucide-react'; - -const getBestIndexes = (values: number[], direction: 'high' | 'low') => { - if (values.length === 0) return []; - const normalized = values.filter((value) => typeof value === 'number'); - if (normalized.length === 0) return values.map(() => false); - - const target = direction === 'high' ? Math.max(...normalized) : Math.min(...normalized); - return values.map((value) => value === target); -}; - -export default function ComparePage() { - const searchParams = useSearchParams(); - const [properties, setProperties] = useState([]); - const [error, setError] = useState(null); - const [isLoading, setIsLoading] = useState(true); - - const selectedIds = useMemo(() => { - const rawIds = searchParams?.get('ids') ?? ''; - return rawIds - .split(',') - .map((id) => id.trim()) - .filter(Boolean) - .slice(0, 3); - }, [searchParams]); - useEffect(() => { - const fetchProperties = async () => { - setIsLoading(true); - setError(null); - - try { - const results = await Promise.all( - selectedIds.map((id) => propertyService.getPropertyById(id, { strategy: 'stale-while-revalidate' })) - ); - - setProperties(results.filter((property): property is Property => !!property)); - } catch (err) { - setError('Unable to load comparison data.'); - } finally { - setIsLoading(false); - } - }; - - if (selectedIds.length === 0) { - setProperties([]); - setIsLoading(false); - return; - } - - fetchProperties(); - }, [selectedIds]); - - const numericHighlights = useMemo(() => { - const totalValues = properties.map((property) => property.price.total); - const perToken = properties.map((property) => property.price.perToken); - const roiValues = properties.map((property) => property.metrics.roi); - const annualReturn = properties.map((property) => property.metrics.annualReturn); - const appreciationRate = properties.map((property) => property.metrics.appreciationRate); - const transactionVolume = properties.map((property) => property.metrics.transactionVolume); - const bedrooms = properties.map((property) => property.details.bedrooms ?? 0); - const bathrooms = properties.map((property) => property.details.bathrooms ?? 0); - const squareFeet = properties.map((property) => property.details.squareFeet); - const availableTokens = properties.map((property) => property.tokenInfo.available); - - return { - totalValue: getBestIndexes(totalValues, 'low'), - perToken: getBestIndexes(perToken, 'low'), - roi: getBestIndexes(roiValues, 'high'), - annualReturn: getBestIndexes(annualReturn, 'high'), - appreciationRate: getBestIndexes(appreciationRate, 'high'), - transactionVolume: getBestIndexes(transactionVolume, 'high'), - bedrooms: getBestIndexes(bedrooms, 'high'), - bathrooms: getBestIndexes(bathrooms, 'high'), - squareFeet: getBestIndexes(squareFeet, 'high'), - availableTokens: getBestIndexes(availableTokens, 'high'), - }; - }, [properties]); - - const rows = useMemo( - () => - properties.length > 0 - ? [ - { - label: 'Location', - values: properties.map((property) => `${property.location.city}, ${property.location.state}`), - }, - { - label: 'Blockchain', - values: properties.map((property) => property.blockchain), - }, - { - label: 'Total Value', - values: properties.map((property) => formatPrice(property.price.total)), - highlight: numericHighlights.totalValue, - }, - { - label: 'Price per Token', - values: properties.map((property) => formatPrice(property.price.perToken)), - highlight: numericHighlights.perToken, - }, - { - label: 'ROI', - values: properties.map((property) => formatROI(property.metrics.roi)), - highlight: numericHighlights.roi, - }, - { - label: 'Annual Return', - values: properties.map((property) => formatPrice(property.metrics.annualReturn)), - highlight: numericHighlights.annualReturn, - }, - { - label: 'Appreciation Rate', - values: properties.map((property) => `${property.metrics.appreciationRate}%`), - highlight: numericHighlights.appreciationRate, - }, - { - label: 'Transaction Volume', - values: properties.map((property) => formatPrice(property.metrics.transactionVolume)), - highlight: numericHighlights.transactionVolume, - }, - { - label: 'Bedrooms', - values: properties.map((property) => String(property.details.bedrooms ?? '—')), - highlight: numericHighlights.bedrooms, - }, - { - label: 'Bathrooms', - values: properties.map((property) => String(property.details.bathrooms ?? '—')), - highlight: numericHighlights.bathrooms, - }, - { - label: 'Square Feet', - values: properties.map((property) => property.details.squareFeet.toLocaleString()), - highlight: numericHighlights.squareFeet, - }, - { - label: 'Available Tokens', - values: properties.map((property) => property.tokenInfo.available.toLocaleString()), - highlight: numericHighlights.availableTokens, - }, - ] - : [] , - [properties, numericHighlights] - ); - - return ( -
-
-
-
-

Comparison

-

Property Comparison

-

- Compare up to 3 selected properties across key investment metrics and share your results via URL. -

-
- -
- - - Select More Properties - - {selectedIds.length > 0 && ( - 0 && ( + - - {isLoading ? ( -
- Loading selected properties... -
- ) : error ? ( -
-

{error}

-
- ) : properties.length === 0 ? ( -
-

No properties selected for comparison.

-

Choose properties from the listing page and use the compare checkbox to start a new comparison.

-
- ) : ( -
-
-
- - - - - {properties.map((property) => ( - - ))} - - - - {rows.map((row) => ( - - - {row.values.map((value, index) => { - const isBest = row.highlight?.[index]; - return ( - - ); - })} - + + Clear All + + +
+ {history.map((comp) => ( +
+
+
+ + {formatTimestamp(comp.timestamp)} +
+ +
+
+ {comp.propertyIds.map((id) => ( +
+ Property #{id} +
))} -
-
Metric -
-
- {property.name} -
-
-

{property.name}

-

{property.location.city}, {property.location.state}

-
-
-
{row.label} - {value} -
-
+
+ + View Comparison + +
+ ))}
)}
-
+ ); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 54aee163..f324170d 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -26,6 +26,7 @@ import { WalletInfo } from "@/components/homepage/WalletInfo"; import { ChainFeatures } from "@/components/homepage/ChainFeatures"; import { TransactionDemo } from "@/components/homepage/TransactionDemo"; import { MultiChainFeatures } from "@/components/homepage/MultiChainFeatures"; +import { RecentlyViewed } from "@/components/RecentlyViewed"; function HomeContent() { const { t } = useTranslation("common"); @@ -182,6 +183,9 @@ function HomeContent() { ))} + + {/* Recently Viewed Properties */} + ); diff --git a/src/components/ClientProviders.tsx b/src/components/ClientProviders.tsx index 2308ae49..71ca324a 100644 --- a/src/components/ClientProviders.tsx +++ b/src/components/ClientProviders.tsx @@ -7,6 +7,7 @@ import { QueryProvider } from "@/providers/QueryProvider"; import { PerformanceMonitor } from "@/components/PerformanceMonitor"; import { ServiceWorkerRegistration } from "@/components/ServiceWorkerRegistration"; import { OfflineIndicator } from "@/components/OfflineIndicator"; +import { LoadingProgressBar } from "@/components/LoadingProgressBar"; import "@/lib/i18n"; import dynamic from "next/dynamic"; @@ -35,6 +36,7 @@ export function ClientProviders({ children }: ClientProvidersProps) { return ( + {children} @@ -44,6 +46,7 @@ export function ClientProviders({ children }: ClientProvidersProps) { + diff --git a/src/components/LoadingProgressBar.tsx b/src/components/LoadingProgressBar.tsx new file mode 100644 index 00000000..2c7f43c2 --- /dev/null +++ b/src/components/LoadingProgressBar.tsx @@ -0,0 +1,96 @@ +'use client'; + +import { useEffect, useState, useRef } from 'react'; +import { usePathname, useSearchParams } from 'next/navigation'; + +interface LoadingProgressBarProps { + color?: string; + height?: number; + duration?: number; +} + +export const LoadingProgressBar: React.FC = ({ + color = '#2563eb', // Brand blue color + height = 3, + duration = 300, +}) => { + const [progress, setProgress] = useState(0); + const [isVisible, setIsVisible] = useState(false); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const timerRef = useRef(null); + const animationRef = useRef(null); + + useEffect(() => { + // Check if user prefers reduced motion + const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + if (prefersReducedMotion) { + return; + } + + // Start progress on route change + setIsVisible(true); + setProgress(0); + + // Simulate progress increase + let currentProgress = 0; + const increment = () => { + currentProgress += Math.random() * 15; + if (currentProgress > 90) { + currentProgress = 90; // Cap at 90% until route change completes + } + setProgress(currentProgress); + }; + + timerRef.current = setInterval(increment, 100); + + return () => { + if (timerRef.current) { + clearInterval(timerRef.current); + } + }; + }, [pathname, searchParams]); + + useEffect(() => { + if (progress > 0 && progress < 100) { + // Complete the progress when route changes + setProgress(100); + + const completeTimer = setTimeout(() => { + setIsVisible(false); + setProgress(0); + }, duration); + + return () => clearTimeout(completeTimer); + } + + // Return undefined explicitly when condition is not met + return undefined; + }, [pathname, searchParams, progress, duration]); + + if (!isVisible) return null; + + return ( +
+
+
+ ); +}; diff --git a/src/components/MultiCurrencyBalance.tsx b/src/components/MultiCurrencyBalance.tsx new file mode 100644 index 00000000..9b27a509 --- /dev/null +++ b/src/components/MultiCurrencyBalance.tsx @@ -0,0 +1,227 @@ +'use client'; + +import React, { useState, useEffect } from 'react'; +import { Wallet, DollarSign, ChevronDown } from 'lucide-react'; +import { useWalletStore } from '@/store/walletStore'; +import { useChain } from '@/providers/ChainAwareProvider'; + +interface TokenBalance { + symbol: string; + name: string; + balance: string; + usdValue?: string; + price?: number; +} + +export const MultiCurrencyBalance: React.FC = () => { + const { balance, address, chainId } = useWalletStore(); + const { chainConfig } = useChain(); + const [showDropdown, setShowDropdown] = useState(false); + const [displayCurrency, setDisplayCurrency] = useState<'native' | 'usd'>('native'); + const [tokenBalances, setTokenBalances] = useState([]); + const [usdPrice, setUsdPrice] = useState(0); + + useEffect(() => { + if (!address || !balance) return; + + // Fetch USD price for the native token + fetchUsdPrice(); + + // Simulate fetching multiple token balances + fetchTokenBalances(); + }, [address, balance, chainId]); + + const fetchUsdPrice = async () => { + try { + // In production, use a real price API like CoinGecko + const mockPrices: Record = { + 1: 3000, // ETH + 137: 0.85, // MATIC + 56: 600, // BNB + }; + const price = mockPrices[chainId] || 2000; + setUsdPrice(price); + } catch (error) { + console.error('Failed to fetch USD price:', error); + } + }; + + const fetchTokenBalances = async () => { + if (!address) return; + + // Mock token balances - in production, fetch from blockchain + const balances: TokenBalance[] = [ + { + symbol: chainConfig.symbol, + name: chainConfig.name, + balance: balance || '0', + usdValue: (parseFloat(balance || '0') * usdPrice).toFixed(2), + price: usdPrice, + }, + ]; + + // Add other tokens if on Ethereum mainnet + if (chainId === 1) { + balances.push({ + symbol: 'USDT', + name: 'Tether USD', + balance: '150.00', + usdValue: '150.00', + price: 1, + }); + balances.push({ + symbol: 'USDC', + name: 'USD Coin', + balance: '250.00', + usdValue: '250.00', + price: 1, + }); + } + + setTokenBalances(balances); + }; + + const getNativeBalance = () => { + return tokenBalances.find((t) => t.symbol === chainConfig.symbol); + }; + + const getTotalUsdValue = () => { + return tokenBalances.reduce((total, token) => { + return total + parseFloat(token.usdValue || '0'); + }, 0); + }; + + const isLowBalance = () => { + const nativeBalance = getNativeBalance(); + return nativeBalance && parseFloat(nativeBalance.balance) < 0.01; + }; + + if (!address) return null; + + const nativeBalance = getNativeBalance(); + + return ( +
+ + + {showDropdown && ( + <> +
setShowDropdown(false)} + /> +
+
+
+

+ Wallet Balance +

+
+ + +
+
+
+ ${getTotalUsdValue().toFixed(2)} +
+
+ +
+

+ Token Balances +

+
+ {tokenBalances.map((token) => ( +
+
+
+ + {token.symbol.slice(0, 2)} + +
+
+
+ {token.balance} {token.symbol} +
+
+ {token.name} +
+
+
+
+
+ ${token.usdValue} +
+ {token.price && ( +
+ @{token.price.toFixed(2)} +
+ )} +
+
+ ))} +
+
+ + {isLowBalance() && ( +
+
+ +
+

+ Low Balance Warning +

+

+ Your {chainConfig.symbol} balance is low. You may need more for transaction fees. +

+
+
+
+ )} +
+ + )} +
+ ); +}; diff --git a/src/components/PropertyDetail.tsx b/src/components/PropertyDetail.tsx index 421dd301..63456bc4 100644 --- a/src/components/PropertyDetail.tsx +++ b/src/components/PropertyDetail.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useEffect } from 'react'; import Image from 'next/image'; import { usePropertyQuery } from '@/hooks/usePropertySearchQuery'; import { Button } from '@/components/ui/button'; @@ -11,6 +11,7 @@ import { formatPrice, formatROI, getBlockchainColor, getPropertyTypeIcon } from import { BLOCKCHAIN_LABELS, PROPERTY_TYPE_LABELS, type PriceAlertType } from '@/types/property'; import { useCartStore } from '@/store/cartStore'; import { useNotificationStore } from '@/store/notificationStore'; +import { useRecentlyViewedStore } from '@/store/recentlyViewedStore'; import { toast } from 'sonner'; import { MortgageCalculator } from '@/components/MortgageCalculator'; import { Loader2, ArrowLeft } from 'lucide-react'; @@ -24,6 +25,7 @@ interface PropertyDetailProps { export const PropertyDetail: React.FC = ({ propertyId }) => { const { addItem } = useCartStore(); const { priceAlerts, addPriceAlert } = useNotificationStore(); + const { addProperty: addRecentlyViewed } = useRecentlyViewedStore(); const [isAlertModalOpen, setIsAlertModalOpen] = useState(false); const { @@ -32,6 +34,19 @@ export const PropertyDetail: React.FC = ({ propertyId }) => error } = usePropertyQuery(propertyId); + // Track property view + useEffect(() => { + if (property) { + addRecentlyViewed({ + id: property.id, + name: property.name, + location: `${property.location.city}, ${property.location.state}`, + price: property.price.total, + image: property.images[0], + }); + } + }, [property, addRecentlyViewed]); + // Check if there's an existing alert for this property const existingAlert = priceAlerts.find(alert => alert.propertyId === propertyId); diff --git a/src/components/RecentlyViewed.tsx b/src/components/RecentlyViewed.tsx new file mode 100644 index 00000000..66e22056 --- /dev/null +++ b/src/components/RecentlyViewed.tsx @@ -0,0 +1,91 @@ +'use client'; + +import React from 'react'; +import Image from 'next/image'; +import Link from 'next/link'; +import { Clock, Trash2, X } from 'lucide-react'; +import { useRecentlyViewedStore } from '@/store/recentlyViewedStore'; +import { formatPrice } from '@/utils/searchUtils'; + +export const RecentlyViewed: React.FC = () => { + const { properties, removeProperty, clearHistory } = useRecentlyViewedStore(); + + if (properties.length === 0) return null; + + const formatTimeAgo = (timestamp: number) => { + const now = Date.now(); + const diffMs = now - timestamp; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMs / 3600000); + const diffDays = Math.floor(diffMs / 86400000); + + if (diffMins < 1) return 'Just now'; + if (diffMins < 60) return `${diffMins}m ago`; + if (diffHours < 24) return `${diffHours}h ago`; + return `${diffDays}d ago`; + }; + + return ( +
+
+
+ +

+ Recently Viewed +

+
+ +
+ +
+ {properties.map((property) => ( +
+ +
+ {property.name} +
+
+

+ {property.name} +

+

+ {property.location} +

+

+ {formatPrice(property.price)} +

+

+ {formatTimeAgo(property.viewedAt)} +

+
+ + +
+ ))} +
+
+ ); +}; diff --git a/src/components/WalletConnector.tsx b/src/components/WalletConnector.tsx index c16c5d36..80cab441 100644 --- a/src/components/WalletConnector.tsx +++ b/src/components/WalletConnector.tsx @@ -7,6 +7,7 @@ import { useChain } from '@/providers/ChainAwareProvider'; import { logger } from '@/utils/logger'; import { useKycStore } from '@/store/kycStore'; import { KycStatusBadge } from '@/components/kyc/KycStatusBadge'; +import { MultiCurrencyBalance } from '@/components/MultiCurrencyBalance'; const WalletModal = dynamic( () => import("./WalletModal").then((m) => m.WalletModal), @@ -72,18 +73,7 @@ export const WalletConnector: React.FC = () => {
-
-
- - {chainConfig.symbol} - - - {parseFloat(useWalletStore.getState().balance || '0').toFixed(3)} - -
+
diff --git a/src/store/comparisonHistoryStore.ts b/src/store/comparisonHistoryStore.ts new file mode 100644 index 00000000..b1268575 --- /dev/null +++ b/src/store/comparisonHistoryStore.ts @@ -0,0 +1,66 @@ +'use client'; + +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +export interface ComparisonHistory { + id: string; + propertyIds: string[]; + timestamp: number; + shareUrl: string; +} + +interface ComparisonHistoryStore { + history: ComparisonHistory[]; + addComparison: (propertyIds: string[]) => void; + removeComparison: (id: string) => void; + clearHistory: () => void; + getHistory: () => ComparisonHistory[]; +} + +const MAX_HISTORY = 5; + +export const useComparisonHistoryStore = create()( + persist( + (set, get) => ({ + history: [], + + addComparison: (propertyIds: string[]) => { + if (propertyIds.length === 0) return; + + const id = `comp_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`; + const shareUrl = `/compare?ids=${propertyIds.join(',')}`; + + const newComparison: ComparisonHistory = { + id, + propertyIds, + timestamp: Date.now(), + shareUrl, + }; + + set((state) => { + const updatedHistory = [newComparison, ...state.history].slice(0, MAX_HISTORY); + return { history: updatedHistory }; + }); + }, + + removeComparison: (id: string) => { + set((state) => ({ + history: state.history.filter((item) => item.id !== id), + })); + }, + + clearHistory: () => { + set({ history: [] }); + }, + + getHistory: () => { + return get().history; + }, + }), + { + name: 'propchain-comparison-history', + partialize: (state) => ({ history: state.history }), + } + ) +); diff --git a/src/store/recentlyViewedStore.ts b/src/store/recentlyViewedStore.ts new file mode 100644 index 00000000..e1b9e2ac --- /dev/null +++ b/src/store/recentlyViewedStore.ts @@ -0,0 +1,62 @@ +'use client'; + +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +export interface RecentlyViewedProperty { + id: string; + name: string; + location: string; + price: number; + image: string; + viewedAt: number; +} + +interface RecentlyViewedStore { + properties: RecentlyViewedProperty[]; + addProperty: (property: Omit) => void; + removeProperty: (id: string) => void; + clearHistory: () => void; + getProperties: () => RecentlyViewedProperty[]; +} + +const MAX_RECENT = 10; + +export const useRecentlyViewedStore = create()( + persist( + (set, get) => ({ + properties: [], + + addProperty: (property) => { + set((state) => { + // Remove if already exists + const filtered = state.properties.filter((p) => p.id !== property.id); + // Add to beginning and limit to MAX_RECENT + const updated = [ + { ...property, viewedAt: Date.now() }, + ...filtered, + ].slice(0, MAX_RECENT); + return { properties: updated }; + }); + }, + + removeProperty: (id: string) => { + set((state) => ({ + properties: state.properties.filter((p) => p.id !== id), + })); + }, + + clearHistory: () => { + set({ properties: [] }); + }, + + getProperties: () => { + return get().properties; + }, + }), + { + name: 'propchain-recently-viewed', + partialize: (state) => ({ properties: state.properties }), + } + ) +);