irongit

test(database): fix the id mix up on local deploy

huncholanehuncholaneauthored
parent c2f498fcommit 2e7e0c22276fc9ec00942623104271e1d946de83Browse files

5 files changed, +254 -58

+3-2scripts/deploy.js
@@ -160,8 +160,9 @@ async function seedDatabase(wallets) {
160160 const propertyIds = [];
161161 for (let i = 0; i < SAMPLE_PROPERTIES.length; i++) {
162162 const p = SAMPLE_PROPERTIES[i];
163- const cityId = cities[i % cities.length].id;
164- const stateId = states[i % states.length].id;
163+ const city = cities[i % cities.length];
164+ const cityId = city.id;
165+ const stateId = city.state_id; // Use the city's actual state, not a separate cycle
165166 const typeId = propertyTypes[i % propertyTypes.length].id;
166167 const slug = p.title.toLowerCase().replace(/\s+/g, '-');
167168
+110-15src/components/layout/Navbar.jsx
@@ -1,19 +1,25 @@
1-import { useState } from 'react';
1+import { useState, useRef, useEffect } from 'react';
22 import { Link } from 'react-router-dom';
3-import { FiMenu, FiX } from 'react-icons/fi';
3+import { FiMenu, FiX, FiChevronDown, FiLogOut, FiCopy, FiCheck, FiExternalLink } from 'react-icons/fi';
44 import { useWallet } from '../../hooks/useWallet';
55
66 function Navbar() {
77 const [isOpen, setIsOpen] = useState(false);
8+ const [showWalletMenu, setShowWalletMenu] = useState(false);
9+ const [copied, setCopied] = useState(false);
10+ const walletMenuRef = useRef(null);
11+
812 const {
9- connect,
13+ connectWithPicker,
1014 disconnect,
1115 shortAddress,
16+ address,
1217 isConnected,
1318 isConnecting,
1419 isCorrectNetwork,
1520 switchToHardhat,
16- hasMetaMask
21+ hasMetaMask,
22+ chainId
1723 } = useWallet();
1824
1925 const navigation = [
@@ -24,11 +30,35 @@ function Navbar() {
2430 { name: 'Blog', href: '/blog' },
2531 ];
2632
27- const handleConnect = async () => {
28- if (isConnected) {
29- disconnect();
30- } else {
31- await connect();
33+ // Close wallet menu when clicking outside
34+ useEffect(() => {
35+ function handleClickOutside(event) {
36+ if (walletMenuRef.current && !walletMenuRef.current.contains(event.target)) {
37+ setShowWalletMenu(false);
38+ }
39+ }
40+ document.addEventListener('mousedown', handleClickOutside);
41+ return () => document.removeEventListener('mousedown', handleClickOutside);
42+ }, []);
43+
44+ const copyAddress = () => {
45+ navigator.clipboard.writeText(address);
46+ setCopied(true);
47+ setTimeout(() => setCopied(false), 2000);
48+ };
49+
50+ const handleDisconnect = () => {
51+ disconnect();
52+ setShowWalletMenu(false);
53+ };
54+
55+ const getNetworkName = () => {
56+ switch (chainId) {
57+ case 1: return 'Ethereum';
58+ case 31337: return 'Hardhat';
59+ case 5: return 'Goerli';
60+ case 11155111: return 'Sepolia';
61+ default: return `Chain ${chainId}`;
3262 }
3363 };
3464
@@ -50,23 +80,90 @@ function Navbar() {
5080 return (
5181 <button
5282 onClick={switchToHardhat}
53- className={mobile ? "block mx-2 mt-2 px-3 py-2 text-base font-medium btn-secondary w-auto" : "btn-secondary"}
83+ className={mobile ? "block mx-2 mt-2 px-3 py-2 text-base font-medium btn-secondary w-auto" : "btn-secondary text-sm"}
5484 >
5585 Wrong Network
5686 </button>
5787 );
5888 }
5989
90+ if (isConnected) {
91+ return (
92+ <div className="relative" ref={walletMenuRef}>
93+ <button
94+ onClick={() => setShowWalletMenu(!showWalletMenu)}
95+ className={mobile
96+ ? "flex items-center justify-between w-full mx-2 mt-2 px-3 py-2 text-base font-medium btn"
97+ : "btn flex items-center space-x-2"
98+ }
99+ >
100+ <span className="flex items-center">
101+ <span className="w-2 h-2 bg-green-500 rounded-full mr-2"></span>
102+ {shortAddress}
103+ </span>
104+ <FiChevronDown className={`ml-1 transition-transform ${showWalletMenu ? 'rotate-180' : ''}`} />
105+ </button>
106+
107+ {/* Dropdown Menu */}
108+ {showWalletMenu && (
109+ <div className={`absolute ${mobile ? 'left-2 right-2' : 'right-0'} mt-2 w-64 bg-white rounded-xl shadow-lg border border-gray-100 py-2 z-50`}>
110+ {/* Address */}
111+ <div className="px-4 py-2 border-b border-gray-100">
112+ <p className="text-xs text-gray-500">Connected Wallet</p>
113+ <p className="font-mono text-sm truncate">{address}</p>
114+ </div>
115+
116+ {/* Network */}
117+ <div className="px-4 py-2 border-b border-gray-100">
118+ <p className="text-xs text-gray-500">Network</p>
119+ <p className="text-sm font-medium flex items-center">
120+ <span className={`w-2 h-2 rounded-full mr-2 ${isCorrectNetwork ? 'bg-green-500' : 'bg-red-500'}`}></span>
121+ {getNetworkName()}
122+ </p>
123+ </div>
124+
125+ {/* Actions */}
126+ <button
127+ onClick={copyAddress}
128+ className="w-full px-4 py-2 text-left text-sm hover:bg-gray-50 flex items-center"
129+ >
130+ {copied ? <FiCheck className="mr-2 text-green-500" /> : <FiCopy className="mr-2" />}
131+ {copied ? 'Copied!' : 'Copy Address'}
132+ </button>
133+
134+ <a
135+ href={`https://etherscan.io/address/${address}`}
136+ target="_blank"
137+ rel="noopener noreferrer"
138+ className="w-full px-4 py-2 text-left text-sm hover:bg-gray-50 flex items-center"
139+ >
140+ <FiExternalLink className="mr-2" />
141+ View on Explorer
142+ </a>
143+
144+ <button
145+ onClick={handleDisconnect}
146+ className="w-full px-4 py-2 text-left text-sm hover:bg-gray-50 flex items-center text-red-600"
147+ >
148+ <FiLogOut className="mr-2" />
149+ Disconnect
150+ </button>
151+ </div>
152+ )}
153+ </div>
154+ );
155+ }
156+
60157 return (
61158 <button
62- onClick={handleConnect}
159+ onClick={connectWithPicker}
63160 disabled={isConnecting}
64161 className={mobile
65162 ? "block mx-2 mt-2 px-3 py-2 text-base font-medium btn w-auto"
66163 : "btn"
67164 }
68165 >
69- {isConnecting ? 'Connecting...' : isConnected ? shortAddress : 'Connect'}
166+ {isConnecting ? 'Connecting...' : 'Connect'}
70167 </button>
71168 );
72169 };
@@ -126,9 +223,7 @@ function Navbar() {
126223 {item.name}
127224 </Link>
128225 ))}
129- <div onClick={() => setIsOpen(false)}>
130- {renderConnectButton(true)}
131- </div>
226+ {renderConnectButton(true)}
132227 </div>
133228 </div>
134229 )}
+49-14src/hooks/useWallet.js
@@ -11,6 +11,7 @@ import {
1111 walletErrorAtom,
1212 shortAddressAtom,
1313 isCorrectNetworkAtom,
14+ manuallyDisconnectedAtom,
1415 } from '../store/wallet';
1516
1617 export function useWallet() {
@@ -23,12 +24,13 @@ export function useWallet() {
2324 const [error, setError] = useAtom(walletErrorAtom);
2425 const [shortAddress] = useAtom(shortAddressAtom);
2526 const [isCorrectNetwork] = useAtom(isCorrectNetworkAtom);
27+ const [manuallyDisconnected, setManuallyDisconnected] = useAtom(manuallyDisconnectedAtom);
2628
2729 // Check if MetaMask is installed
2830 const hasMetaMask = typeof window !== 'undefined' && Boolean(window.ethereum);
2931
3032 // Connect wallet
31- const connect = useCallback(async () => {
33+ const connect = useCallback(async (forceAccountPicker = false) => {
3234 if (!hasMetaMask) {
3335 setError('Please install MetaMask to connect');
3436 return;
@@ -40,8 +42,18 @@ export function useWallet() {
4042 try {
4143 const web3Provider = new ethers.providers.Web3Provider(window.ethereum);
4244
43- // Request account access
44- const accounts = await window.ethereum.request({
45+ let accounts;
46+
47+ if (forceAccountPicker) {
48+ // Force MetaMask to show account picker by requesting permissions
49+ await window.ethereum.request({
50+ method: 'wallet_requestPermissions',
51+ params: [{ eth_accounts: {} }],
52+ });
53+ }
54+
55+ // Get the selected account
56+ accounts = await window.ethereum.request({
4557 method: 'eth_requestAccounts',
4658 });
4759
@@ -52,6 +64,10 @@ export function useWallet() {
5264 setSigner(web3Signer);
5365 setAddress(accounts[0]);
5466 setChainId(network.chainId);
67+
68+ // Clear the manually disconnected flag
69+ setManuallyDisconnected(false);
70+ sessionStorage.removeItem('walletDisconnected');
5571 } catch (err) {
5672 console.error('Failed to connect wallet:', err);
5773 if (err.code === 4001) {
@@ -62,7 +78,10 @@ export function useWallet() {
6278 } finally {
6379 setIsConnecting(false);
6480 }
65- }, [hasMetaMask, setAddress, setChainId, setError, setIsConnecting, setProvider, setSigner]);
81+ }, [hasMetaMask, setAddress, setChainId, setError, setIsConnecting, setProvider, setSigner, setManuallyDisconnected]);
82+
83+ // Connect with account picker (for user-initiated connects)
84+ const connectWithPicker = useCallback(() => connect(true), [connect]);
6685
6786 // Disconnect wallet
6887 const disconnect = useCallback(() => {
@@ -71,7 +90,11 @@ export function useWallet() {
7190 setSigner(null);
7291 setChainId(null);
7392 setError(null);
74- }, [setAddress, setChainId, setError, setProvider, setSigner]);
93+
94+ // Set flag to prevent auto-reconnect
95+ setManuallyDisconnected(true);
96+ sessionStorage.setItem('walletDisconnected', 'true');
97+ }, [setAddress, setChainId, setError, setProvider, setSigner, setManuallyDisconnected]);
7598
7699 // Switch to Hardhat local network
77100 const switchToHardhat = useCallback(async () => {
@@ -106,17 +129,28 @@ export function useWallet() {
106129 useEffect(() => {
107130 if (!hasMetaMask) return;
108131
109- const handleAccountsChanged = (accounts) => {
132+ const handleAccountsChanged = async (accounts) => {
133+ // If user manually disconnected, don't auto-reconnect
134+ if (manuallyDisconnected) return;
135+
110136 if (accounts.length === 0) {
111137 disconnect();
112- } else {
138+ } else if (isConnected) {
139+ // Only update if already connected (user switched accounts in MetaMask)
140+ const web3Provider = new ethers.providers.Web3Provider(window.ethereum);
141+ const web3Signer = web3Provider.getSigner();
142+
143+ setProvider(web3Provider);
144+ setSigner(web3Signer);
113145 setAddress(accounts[0]);
114146 }
115147 };
116148
117- const handleChainChanged = (chainIdHex) => {
118- const newChainId = parseInt(chainIdHex, 16);
119- setChainId(newChainId);
149+ const handleChainChanged = () => {
150+ // Only reload if connected
151+ if (isConnected) {
152+ window.location.reload();
153+ }
120154 };
121155
122156 window.ethereum.on('accountsChanged', handleAccountsChanged);
@@ -126,11 +160,11 @@ export function useWallet() {
126160 window.ethereum.removeListener('accountsChanged', handleAccountsChanged);
127161 window.ethereum.removeListener('chainChanged', handleChainChanged);
128162 };
129- }, [hasMetaMask, disconnect, setAddress, setChainId]);
163+ }, [hasMetaMask, disconnect, setAddress, setProvider, setSigner, isConnected, manuallyDisconnected]);
130164
131- // Auto-connect if already authorized
165+ // Auto-connect if already authorized (but not if manually disconnected)
132166 useEffect(() => {
133- if (!hasMetaMask || isConnected) return;
167+ if (!hasMetaMask || isConnected || manuallyDisconnected) return;
134168
135169 window.ethereum
136170 .request({ method: 'eth_accounts' })
@@ -140,7 +174,7 @@ export function useWallet() {
140174 }
141175 })
142176 .catch(console.error);
143- }, [hasMetaMask, isConnected, connect]);
177+ }, [hasMetaMask, isConnected, connect, manuallyDisconnected]);
144178
145179 return {
146180 // State
@@ -156,6 +190,7 @@ export function useWallet() {
156190 hasMetaMask,
157191 // Actions
158192 connect,
193+ connectWithPicker,
159194 disconnect,
160195 switchToHardhat,
161196 };
+87-27src/pages/PropertyDetail.jsx
@@ -21,7 +21,7 @@ const ContractStates = {
2121
2222 function PropertyDetail() {
2323 const { id } = useParams(); // This is actually the slug from the URL
24- const { connect, isConnected, isConnecting, shortAddress, address } = useWallet();
24+ const { connect, connectWithPicker, isConnected, isConnecting, shortAddress, address } = useWallet();
2525 const { signAsBuyerWithDeposit, signAsSeller, loading: contractLoading, error: contractError } = useContracts();
2626
2727 const [property, setProperty] = useState(null);
@@ -312,43 +312,91 @@ function PropertyDetail() {
312312 </div>
313313 </div>
314314
315- {/* Investment Input */}
316- {contractStateInfo?.canInvest && (
315+ {/* Investment Section - Always visible with state-appropriate messaging */}
316+ {property.contractState && (
317317 <div className="mb-4">
318- <label className="block text-sm font-medium text-secondary-700 mb-2">
319- Investment Amount (ETH)
320- </label>
321- <div className="flex space-x-2">
322- <input
323- type="number"
324- step="0.01"
325- min={minDeposit}
326- max={maxDeposit}
327- value={investAmount}
328- onChange={(e) => setInvestAmount(e.target.value)}
329- placeholder={`Min: ${minDeposit.toFixed(2)} ETH`}
330- className="input flex-1"
331- />
332- </div>
333- <p className="text-xs text-secondary-500 mt-1">
334- Deposit range: {minDeposit.toFixed(2)} - {maxDeposit.toFixed(2)} ETH (10-100% of price)
335- </p>
318+ {/* State 0: Waiting for Seller */}
319+ {property.contractState.state === 0 && (
320+ <div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
321+ <p className="text-sm font-medium text-yellow-800 mb-2">Awaiting Seller Signature</p>
322+ <p className="text-xs text-yellow-700">
323+ The seller must sign this contract before investments can be made.
324+ </p>
325+ <p className="text-xs text-yellow-600 mt-2 font-mono">
326+ Seller: {property.contractState.seller?.slice(0, 10)}...{property.contractState.seller?.slice(-8)}
327+ </p>
328+ </div>
329+ )}
330+
331+ {/* State 1: Can Invest */}
332+ {property.contractState.state === 1 && (
333+ <>
334+ <label className="block text-sm font-medium text-secondary-700 mb-2">
335+ Investment Amount (ETH)
336+ </label>
337+ <div className="flex space-x-2">
338+ <input
339+ type="number"
340+ step="0.01"
341+ min={minDeposit}
342+ max={maxDeposit}
343+ value={investAmount}
344+ onChange={(e) => setInvestAmount(e.target.value)}
345+ placeholder={`Min: ${minDeposit.toFixed(2)} ETH`}
346+ className="input flex-1"
347+ />
348+ </div>
349+ <p className="text-xs text-secondary-500 mt-1">
350+ Deposit range: {minDeposit.toFixed(2)} - {maxDeposit.toFixed(2)} ETH (10-100% of price)
351+ </p>
352+ </>
353+ )}
354+
355+ {/* State 2+: Transaction in progress */}
356+ {property.contractState.state >= 2 && property.contractState.state <= 3 && (
357+ <div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-4">
358+ <p className="text-sm font-medium text-blue-800 mb-2">{contractStateInfo?.label}</p>
359+ <p className="text-xs text-blue-700">
360+ This transaction is in progress. No new investments can be made.
361+ </p>
362+ </div>
363+ )}
364+
365+ {/* State 4: Completed */}
366+ {property.contractState.state === 4 && (
367+ <div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
368+ <p className="text-sm font-medium text-green-800 mb-2">Transaction Complete</p>
369+ <p className="text-xs text-green-700">
370+ This property has been successfully sold.
371+ </p>
372+ </div>
373+ )}
374+
375+ {/* State 5: Rejected */}
376+ {property.contractState.state === 5 && (
377+ <div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
378+ <p className="text-sm font-medium text-red-800 mb-2">Transaction Rejected</p>
379+ <p className="text-xs text-red-700">
380+ This transaction was cancelled or rejected.
381+ </p>
382+ </div>
383+ )}
336384 </div>
337385 )}
338386
339387 {/* Action Buttons based on state and role */}
340388 {!isConnected ? (
341389 <button
342- onClick={connect}
390+ onClick={connectWithPicker}
343391 disabled={isConnecting}
344392 className="btn w-full mb-4 flex items-center justify-center"
345393 >
346394 <FaWallet className="mr-2" />
347- {isConnecting ? 'Connecting...' : 'Connect Wallet'}
395+ {isConnecting ? 'Connecting...' : 'Connect Wallet to Invest'}
348396 </button>
349397 ) : (
350398 <>
351- {/* Seller Sign Button */}
399+ {/* Seller Sign Button - Show to seller in state 0 */}
352400 {userRole === 'seller' && property.contractState?.state === 0 && (
353401 <button
354402 onClick={handleSellerSign}
@@ -360,8 +408,17 @@ function PropertyDetail() {
360408 </button>
361409 )}
362410
363- {/* Buyer Invest Button */}
364- {contractStateInfo?.canInvest && (
411+ {/* Show info for non-sellers in state 0 */}
412+ {userRole !== 'seller' && property.contractState?.state === 0 && (
413+ <div className="bg-secondary-100 rounded-lg p-3 mb-4 text-center">
414+ <p className="text-sm text-secondary-600">
415+ Waiting for seller to sign before you can invest
416+ </p>
417+ </div>
418+ )}
419+
420+ {/* Buyer Invest Button - Only in state 1 */}
421+ {property.contractState?.state === 1 && (
365422 <button
366423 onClick={handleInvest}
367424 disabled={contractLoading || !investAmount}
@@ -379,10 +436,13 @@ function PropertyDetail() {
379436 </>
380437 )}
381438
382- {/* Contract State Info */}
439+ {/* Contract State Badge */}
383440 {contractStateInfo && (
384441 <div className={`p-3 rounded-lg bg-${contractStateInfo.color}-50 border border-${contractStateInfo.color}-200 mb-4`}>
385442 <p className="text-sm font-medium text-center">{contractStateInfo.label}</p>
443+ {userRole && (
444+ <p className="text-xs text-center text-secondary-600 mt-1">You are the {userRole}</p>
445+ )}
386446 </div>
387447 )}
388448
+5-0src/store/wallet.js
@@ -10,6 +10,11 @@ export const chainIdAtom = atom(null);
1010 export const isConnectingAtom = atom(false);
1111 export const isConnectedAtom = atom((get) => get(addressAtom) !== null);
1212
13+// Track if user manually disconnected (persists to prevent auto-reconnect)
14+export const manuallyDisconnectedAtom = atom(
15+ sessionStorage.getItem('walletDisconnected') === 'true'
16+);
17+
1318 // Error state
1419 export const walletErrorAtom = atom(null);
1520