refactor: migrate to new API service and update search functionality
This commit is contained in:
parent
61603a9175
commit
37a6be3504
|
|
@ -0,0 +1,6 @@
|
||||||
|
# .env
|
||||||
|
VITE_API_URL=http://localhost:8000
|
||||||
|
|
||||||
|
# You can also create environment-specific files:
|
||||||
|
# .env.development
|
||||||
|
# .env.production
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@radix-ui/react-slot": "^1.2.2",
|
||||||
"@radix-ui/react-toast": "^1.2.13",
|
"@radix-ui/react-toast": "^1.2.13",
|
||||||
"@tailwindcss/vite": "^4.1.6",
|
"@tailwindcss/vite": "^4.1.6",
|
||||||
|
"axios": "^1.9.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.11.0",
|
"framer-motion": "^12.11.0",
|
||||||
|
|
@ -2548,6 +2549,11 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
|
||||||
|
},
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.4.21",
|
"version": "10.4.21",
|
||||||
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
|
||||||
|
|
@ -2585,6 +2591,16 @@
|
||||||
"postcss": "^8.1.0"
|
"postcss": "^8.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/axios": {
|
||||||
|
"version": "1.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||||
|
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||||
|
"dependencies": {
|
||||||
|
"follow-redirects": "^1.15.6",
|
||||||
|
"form-data": "^4.0.0",
|
||||||
|
"proxy-from-env": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
|
|
@ -2678,7 +2694,6 @@
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
|
|
@ -2793,6 +2808,17 @@
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
|
@ -2900,6 +2926,14 @@
|
||||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/depd": {
|
"node_modules/depd": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
|
@ -2926,7 +2960,6 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.1",
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
"es-errors": "^1.3.0",
|
"es-errors": "^1.3.0",
|
||||||
|
|
@ -2973,7 +3006,6 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
|
|
@ -2982,7 +3014,6 @@
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
|
|
@ -2991,7 +3022,6 @@
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"es-errors": "^1.3.0"
|
"es-errors": "^1.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -2999,6 +3029,20 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-set-tostringtag": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.6",
|
||||||
|
"has-tostringtag": "^1.0.2",
|
||||||
|
"hasown": "^2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.25.4",
|
"version": "0.25.4",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
|
||||||
|
|
@ -3453,6 +3497,58 @@
|
||||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/follow-redirects": {
|
||||||
|
"version": "1.15.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
|
||||||
|
"integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"debug": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"es-set-tostringtag": "^2.1.0",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data/node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data/node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
|
@ -3527,7 +3623,6 @@
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
"dev": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
|
|
@ -3545,7 +3640,6 @@
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"call-bind-apply-helpers": "^1.0.2",
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
"es-define-property": "^1.0.1",
|
"es-define-property": "^1.0.1",
|
||||||
|
|
@ -3577,7 +3671,6 @@
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dunder-proto": "^1.0.1",
|
"dunder-proto": "^1.0.1",
|
||||||
"es-object-atoms": "^1.0.0"
|
"es-object-atoms": "^1.0.0"
|
||||||
|
|
@ -3614,7 +3707,6 @@
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
|
|
@ -3646,7 +3738,20 @@
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
"dev": true,
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-tostringtag": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||||
|
"dependencies": {
|
||||||
|
"has-symbols": "^1.0.3"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
},
|
},
|
||||||
|
|
@ -3658,7 +3763,6 @@
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"function-bind": "^1.1.2"
|
"function-bind": "^1.1.2"
|
||||||
},
|
},
|
||||||
|
|
@ -4152,7 +4256,6 @@
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
|
|
@ -4550,6 +4653,11 @@
|
||||||
"node": ">= 0.10"
|
"node": ">= 0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/proxy-from-env": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||||
|
},
|
||||||
"node_modules/punycode": {
|
"node_modules/punycode": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@radix-ui/react-slot": "^1.2.2",
|
||||||
"@radix-ui/react-toast": "^1.2.13",
|
"@radix-ui/react-toast": "^1.2.13",
|
||||||
"@tailwindcss/vite": "^4.1.6",
|
"@tailwindcss/vite": "^4.1.6",
|
||||||
|
"axios": "^1.9.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"framer-motion": "^12.11.0",
|
"framer-motion": "^12.11.0",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||||
import HomePage from "./pages/HomePage";
|
|
||||||
import MigrantProfilePage from "./pages/MigrantProfilePage";
|
import MigrantProfilePage from "./pages/MigrantProfilePage";
|
||||||
import NotFoundPage from "./pages/NotFoundPage";
|
import NotFoundPage from "./pages/NotFoundPage";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
@ -7,6 +6,7 @@ import LoginPage from "./components/LoginPage";
|
||||||
import Migrants from "./components/Migrants";
|
import Migrants from "./components/Migrants";
|
||||||
import ProfileSettings from "./components/ui/ProfileSettings";
|
import ProfileSettings from "./components/ui/ProfileSettings";
|
||||||
import AdminDashboardPage from "./pages/AdminDashboardPage";
|
import AdminDashboardPage from "./pages/AdminDashboardPage";
|
||||||
|
import HomePage from "./pages/HomePage";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -17,7 +17,7 @@ function App() {
|
||||||
<Route path="/admin" element={<AdminDashboardPage />} />
|
<Route path="/admin" element={<AdminDashboardPage />} />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
<Route path="/" element={<HomePage />} />
|
<Route path="/" element={<HomePage />} />
|
||||||
<Route path="/migrant/:id" element={<MigrantProfilePage />} />
|
<Route path="/migrants/:id" element={<MigrantProfilePage />} />
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export default function AdminDashboard() {
|
||||||
// Check if user is authenticated
|
// Check if user is authenticated
|
||||||
const token = localStorage.getItem("adminToken")
|
const token = localStorage.getItem("adminToken")
|
||||||
if (!token) {
|
if (!token) {
|
||||||
navigate("/admin/login")
|
navigate("/admin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,7 +62,7 @@ export default function AdminDashboard() {
|
||||||
localStorage.removeItem("adminToken")
|
localStorage.removeItem("adminToken")
|
||||||
localStorage.removeItem("adminNavigation") // Clear navigation flag on logout
|
localStorage.removeItem("adminNavigation") // Clear navigation flag on logout
|
||||||
|
|
||||||
navigate("/admin/login")
|
navigate("/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,14 +46,14 @@ export default function Migrants() {
|
||||||
localStorage.removeItem("adminNavigation") // Clear navigation flag on logout
|
localStorage.removeItem("adminNavigation") // Clear navigation flag on logout
|
||||||
|
|
||||||
|
|
||||||
navigate("/admin/login")
|
navigate("/login")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check authentication and load data
|
// Check authentication and load data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = localStorage.getItem("adminToken")
|
const token = localStorage.getItem("adminToken")
|
||||||
if (!token) {
|
if (!token) {
|
||||||
navigate("/admin/login")
|
navigate("/login")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,69 +1,55 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, type ChangeEvent, type FormEvent } from "react";
|
import { useState, useEffect, type ChangeEvent, type FormEvent } from "react";
|
||||||
import type { SearchParams } from "@/types/search";
|
import type { SearchParams } from "@/types/search";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Search } from "lucide-react";
|
import { Search } from "lucide-react";
|
||||||
|
|
||||||
// Mock data for autocomplete
|
|
||||||
const ITALIAN_REGIONS = [
|
|
||||||
"Abruzzo",
|
|
||||||
"Basilicata",
|
|
||||||
"Calabria",
|
|
||||||
"Campania",
|
|
||||||
"Emilia-Romagna",
|
|
||||||
"Friuli-Venezia Giulia",
|
|
||||||
"Lazio",
|
|
||||||
"Liguria",
|
|
||||||
"Lombardy",
|
|
||||||
"Marche",
|
|
||||||
"Molise",
|
|
||||||
"Piedmont",
|
|
||||||
"Puglia",
|
|
||||||
"Sardinia",
|
|
||||||
"Sicily",
|
|
||||||
"Tuscany",
|
|
||||||
"Trentino-Alto Adige",
|
|
||||||
"Umbria",
|
|
||||||
"Valle d'Aosta",
|
|
||||||
"Veneto",
|
|
||||||
];
|
|
||||||
|
|
||||||
const NT_SETTLEMENTS = [
|
|
||||||
"Darwin",
|
|
||||||
"Alice Springs",
|
|
||||||
"Katherine",
|
|
||||||
"Tennant Creek",
|
|
||||||
"Nhulunbuy",
|
|
||||||
"Jabiru",
|
|
||||||
"Yulara",
|
|
||||||
"Borroloola",
|
|
||||||
"Pine Creek",
|
|
||||||
"Adelaide River",
|
|
||||||
];
|
|
||||||
|
|
||||||
interface SearchFormProps {
|
interface SearchFormProps {
|
||||||
onSearch: (params: SearchParams) => void;
|
onSearch: (params: SearchParams) => void;
|
||||||
|
onReset?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SearchForm = ({ onSearch }: SearchFormProps) => {
|
// Default form data
|
||||||
const [formData, setFormData] = useState<SearchParams>({
|
const defaultData: SearchParams = {
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
ageAtMigration: "",
|
ageAtMigration: "",
|
||||||
yearOfArrival: "",
|
yearOfArrival: "",
|
||||||
regionOfOrigin: "all",
|
regionOfOrigin: "all",
|
||||||
settlementLocation: "all",
|
settlementLocation: "all",
|
||||||
});
|
};
|
||||||
|
|
||||||
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
// Form field definitions to make the JSX cleaner
|
||||||
const { name, value } = e.target;
|
const textFields = [
|
||||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
{ id: "firstName", label: "First Name (Christian Name)", type: "text" },
|
||||||
};
|
{ id: "lastName", label: "Last Name (Surname)", type: "text" },
|
||||||
|
{ id: "ageAtMigration", label: "Age at Migration", type: "number", min: 0, max: 120 },
|
||||||
|
{
|
||||||
|
id: "yearOfArrival",
|
||||||
|
label: "Date of Arrival in NT (Year)",
|
||||||
|
type: "number",
|
||||||
|
min: 1800,
|
||||||
|
max: new Date().getFullYear()
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const handleSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
const selectFields = [
|
||||||
|
{ id: "regionOfOrigin", label: "Region of Origin", options: ["all"] },
|
||||||
|
{ id: "settlementLocation", label: "Settlement Location", options: ["all"] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const SearchForm = ({ onSearch, onReset }: SearchFormProps) => {
|
||||||
|
const [formData, setFormData] = useState(defaultData);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleChange = (e: ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
const { name, value } = e.target;
|
const { name, value } = e.target;
|
||||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
setFormData(prevData => ({ ...prevData, [name]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = (e: FormEvent) => {
|
const handleSubmit = (e: FormEvent) => {
|
||||||
|
|
@ -72,125 +58,57 @@ const SearchForm = ({ onSearch }: SearchFormProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setFormData({
|
setFormData(defaultData);
|
||||||
firstName: "",
|
if (onReset) onReset();
|
||||||
lastName: "",
|
|
||||||
ageAtMigration: "",
|
|
||||||
yearOfArrival: "",
|
|
||||||
regionOfOrigin: "all",
|
|
||||||
settlementLocation: "all",
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="card p-6 mb-8">
|
<form onSubmit={handleSubmit} className="card p-6 mb-8">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
<div className="space-y-2">
|
{/* Text and number input fields */}
|
||||||
<label htmlFor="firstName" className="block text-sm font-medium">
|
{textFields.map(({ id, label, type, min, max }) => (
|
||||||
First Name
|
<div key={id} className="space-y-2">
|
||||||
|
<label htmlFor={id} className="block text-sm font-medium">
|
||||||
|
{label}
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
id="firstName"
|
id={id}
|
||||||
name="firstName"
|
name={id}
|
||||||
type="text"
|
type={type}
|
||||||
value={formData.firstName}
|
value={formData[id as keyof SearchParams] || ""}
|
||||||
onChange={handleInputChange}
|
onChange={handleChange}
|
||||||
placeholder="Enter first name"
|
placeholder={`Enter ${label.toLowerCase()}`}
|
||||||
|
min={min}
|
||||||
|
max={max}
|
||||||
className="w-full p-2 border border-gray-300 rounded-md"
|
className="w-full p-2 border border-gray-300 rounded-md"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
<div className="space-y-2">
|
{selectFields.map(({ id, label, options }) => (
|
||||||
<label htmlFor="lastName" className="block text-sm font-medium">
|
<div key={id} className="space-y-2">
|
||||||
Last Name
|
<label htmlFor={id} className="block text-sm font-medium">
|
||||||
</label>
|
{label}
|
||||||
<input
|
|
||||||
id="lastName"
|
|
||||||
name="lastName"
|
|
||||||
type="text"
|
|
||||||
value={formData.lastName}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
placeholder="Enter last name"
|
|
||||||
className="w-full p-2 border border-gray-300 rounded-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="ageAtMigration" className="block text-sm font-medium">
|
|
||||||
Age at Migration
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="ageAtMigration"
|
|
||||||
name="ageAtMigration"
|
|
||||||
type="number"
|
|
||||||
value={formData.ageAtMigration}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
placeholder="Enter age"
|
|
||||||
min="0"
|
|
||||||
max="120"
|
|
||||||
className="w-full p-2 border border-gray-300 rounded-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="yearOfArrival" className="block text-sm font-medium">
|
|
||||||
Year of Arrival
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="yearOfArrival"
|
|
||||||
name="yearOfArrival"
|
|
||||||
type="number"
|
|
||||||
value={formData.yearOfArrival}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
placeholder="Enter year"
|
|
||||||
min="1800"
|
|
||||||
max={new Date().getFullYear()}
|
|
||||||
className="w-full p-2 border border-gray-300 rounded-md"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label htmlFor="regionOfOrigin" className="block text-sm font-medium">
|
|
||||||
Region of Origin in Italy
|
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="regionOfOrigin"
|
id={id}
|
||||||
name="regionOfOrigin"
|
name={id}
|
||||||
value={formData.regionOfOrigin}
|
value={formData[id as keyof SearchParams] || "all"}
|
||||||
onChange={handleSelectChange}
|
onChange={handleChange}
|
||||||
className="w-full p-2 border border-gray-300 rounded-md"
|
className="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<option value="all">All Regions</option>
|
{options.map(opt => (
|
||||||
{ITALIAN_REGIONS.map((region) => (
|
<option key={opt} value={opt}>
|
||||||
<option key={region} value={region}>
|
{opt === "all" ? `All ${label.split(" ")[0]}s` : opt}
|
||||||
{region}
|
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
{isLoading && (
|
||||||
|
<p className="text-xs text-gray-500">Loading {label.toLowerCase()}...</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label
|
|
||||||
htmlFor="settlementLocation"
|
|
||||||
className="block text-sm font-medium"
|
|
||||||
>
|
|
||||||
Settlement Location in NT
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="settlementLocation"
|
|
||||||
name="settlementLocation"
|
|
||||||
value={formData.settlementLocation}
|
|
||||||
onChange={handleSelectChange}
|
|
||||||
className="w-full p-2 border border-gray-300 rounded-md"
|
|
||||||
>
|
|
||||||
<option value="all">All Locations</option>
|
|
||||||
{NT_SETTLEMENTS.map((location) => (
|
|
||||||
<option key={location} value={location}>
|
|
||||||
{location}
|
|
||||||
</option>
|
|
||||||
))}
|
))}
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 mt-8 justify-end">
|
<div className="flex flex-wrap gap-4 mt-8 justify-end">
|
||||||
|
|
@ -205,6 +123,7 @@ const SearchForm = ({ onSearch }: SearchFormProps) => {
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-gradient-to-r from-green-600 via-white to-red-600 text-gray-800 hover:from-green-700 hover:via-gray-100 hover:to-red-700 font-medium"
|
className="bg-gradient-to-r from-green-600 via-white to-red-600 text-gray-800 hover:from-green-700 hover:via-gray-100 hover:to-red-700 font-medium"
|
||||||
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
<Search className="mr-2 h-4 w-4" /> Search Records
|
<Search className="mr-2 h-4 w-4" /> Search Records
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Link } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import type { SearchResult } from "@/types/search";
|
import type { SearchResult } from "@/types/search";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
|
@ -18,6 +18,9 @@ export default function SearchResults({
|
||||||
isLoading,
|
isLoading,
|
||||||
hasSearched = false,
|
hasSearched = false,
|
||||||
}: SearchResultsProps) {
|
}: SearchResultsProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Define animation variants
|
||||||
const container = {
|
const container = {
|
||||||
hidden: { opacity: 0 },
|
hidden: { opacity: 0 },
|
||||||
show: {
|
show: {
|
||||||
|
|
@ -99,16 +102,15 @@ export default function SearchResults({
|
||||||
animate="show"
|
animate="show"
|
||||||
>
|
>
|
||||||
{results.map((person) => (
|
{results.map((person) => (
|
||||||
<motion.div key={person.id} variants={item}>
|
<motion.div key={person.person_id || person.id_card_no} variants={item} onClick={() => navigate(`/migrants/${person.person_id}`)}>
|
||||||
<Link to={`/migrant/${person.id}`} className="block h-full">
|
<div className="block h-full cursor-pointer">
|
||||||
<Card className="overflow-hidden hover:shadow-lg transition-shadow h-full border border-gray-200 group">
|
<Card className="overflow-hidden hover:shadow-lg transition-shadow h-full border border-gray-200 group">
|
||||||
<div className="relative h-48 w-full overflow-hidden">
|
<div className="relative h-48 w-full overflow-hidden">
|
||||||
<AnimatedImage
|
<AnimatedImage
|
||||||
src={
|
src={
|
||||||
person.photoUrl ||
|
|
||||||
"/placeholder.svg?height=300&width=300"
|
"/placeholder.svg?height=300&width=300"
|
||||||
}
|
}
|
||||||
alt={`${person.firstName} ${person.lastName}`}
|
alt=""
|
||||||
fill
|
fill
|
||||||
/>
|
/>
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||||
|
|
@ -122,27 +124,31 @@ export default function SearchResults({
|
||||||
</div>
|
</div>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="font-serif">
|
<CardTitle className="font-serif">
|
||||||
{person.firstName} {person.lastName}
|
{person.full_name}
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Arrived {person.yearOfArrival} at age{" "}
|
{person.migration?.date_of_arrival_nt ?
|
||||||
{person.ageAtMigration}
|
`Arrived ${new Date(person.migration.date_of_arrival_nt).getFullYear()}` : 'Date unknown'}
|
||||||
</p>
|
</p>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">From:</span>{" "}
|
<span className="font-medium">From:</span>{" "}
|
||||||
{person.regionOfOrigin}, Italy
|
{person.place_of_birth || 'Unknown'}, Italy
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">Settled in:</span>{" "}
|
<span className="font-medium">Settled in:</span>{" "}
|
||||||
{person.settlementLocation}, NT
|
{person.residence?.town_or_city || 'Unknown'}, NT
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Occupation:</span>{" "}
|
||||||
|
{person.occupation || 'Unknown'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</Link>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,69 @@ import { useState } from "react";
|
||||||
import SearchForm from "./SearchForm";
|
import SearchForm from "./SearchForm";
|
||||||
import SearchResults from "./SearchResults";
|
import SearchResults from "./SearchResults";
|
||||||
import type { SearchParams, SearchResult } from "../types/search";
|
import type { SearchParams, SearchResult } from "../types/search";
|
||||||
import { searchMigrants } from "../services/migrantService";
|
import apiService from "../services/apiService";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
|
||||||
const SearchSection = () => {
|
const SearchSection = () => {
|
||||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [hasSearched, setHasSearched] = useState(false);
|
const [hasSearched, setHasSearched] = useState(false);
|
||||||
|
const [noResultsFound, setNoResultsFound] = useState(false);
|
||||||
|
|
||||||
const handleSearch = async (params: SearchParams) => {
|
const handleSearch = async (params: SearchParams) => {
|
||||||
|
// Check if at least one search parameter is provided (not empty and not 'all')
|
||||||
|
const hasSearchCriteria = Object.entries(params).some(([_, value]) =>
|
||||||
|
value && value !== "" && value !== "all"
|
||||||
|
);
|
||||||
|
|
||||||
|
// If no search criteria provided, don't perform search
|
||||||
|
if (!hasSearchCriteria) {
|
||||||
|
setSearchResults([]);
|
||||||
|
setHasSearched(false);
|
||||||
|
setNoResultsFound(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
setIsSearching(true);
|
setIsSearching(true);
|
||||||
|
setSearchResults([]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await searchMigrants(params);
|
// Display a loading message in the UI before making the API call
|
||||||
|
// API call with small delay to ensure loader is visible
|
||||||
|
setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
// Use apiService.searchPeople with exact matching
|
||||||
|
const results = await apiService.searchPeople(params);
|
||||||
setSearchResults(results);
|
setSearchResults(results);
|
||||||
setHasSearched(true);
|
setHasSearched(true);
|
||||||
|
|
||||||
|
// Check if the results array is empty
|
||||||
|
setNoResultsFound(results.length === 0);
|
||||||
|
|
||||||
|
// Log successful search
|
||||||
|
console.log(`Found ${results.length} exact match results using apiService.searchPeople`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error searching migrants:", error);
|
console.error("Error searching people:", error);
|
||||||
// In a real application, you would handle this error more gracefully
|
// Display user-friendly error message
|
||||||
|
setSearchResults([]);
|
||||||
|
setHasSearched(true);
|
||||||
|
setNoResultsFound(true);
|
||||||
} finally {
|
} finally {
|
||||||
setIsSearching(false);
|
setIsSearching(false);
|
||||||
}
|
}
|
||||||
|
}, 500); // Short delay to ensure loader is visible
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Unexpected error:", error);
|
||||||
|
setIsSearching(false);
|
||||||
|
setHasSearched(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setSearchResults([]);
|
||||||
|
setHasSearched(false);
|
||||||
|
setNoResultsFound(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -31,9 +75,31 @@ const SearchSection = () => {
|
||||||
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-center">
|
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-center">
|
||||||
Search Historical Records
|
Search Historical Records
|
||||||
</h2>
|
</h2>
|
||||||
<SearchForm onSearch={handleSearch} />
|
<SearchForm onSearch={handleSearch} onReset={handleReset} />
|
||||||
{(isSearching || hasSearched) && (
|
{isSearching && (
|
||||||
<SearchResults results={searchResults} isLoading={isSearching} />
|
<div className="flex justify-center my-8">
|
||||||
|
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasSearched && !isSearching && noResultsFound && (
|
||||||
|
<div className="text-center py-12 my-8 bg-gray-100 rounded-lg border border-gray-200">
|
||||||
|
<h3 className="text-2xl font-semibold mb-4 font-serif">No Results Found</h3>
|
||||||
|
<p className="text-gray-500 mb-4">
|
||||||
|
We couldn't find any records matching your search criteria.
|
||||||
|
</p>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Try adjusting your search filters or using fewer criteria to broaden your results.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(hasSearched || isSearching) && (
|
||||||
|
<SearchResults
|
||||||
|
results={searchResults}
|
||||||
|
isLoading={isSearching}
|
||||||
|
hasSearched={hasSearched && !noResultsFound}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const alertVariants = cva(
|
||||||
|
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-card text-card-foreground",
|
||||||
|
destructive:
|
||||||
|
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Alert({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert"
|
||||||
|
role="alert"
|
||||||
|
className={cn(alertVariants({ variant }), className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-title"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AlertDescription({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="alert-description"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Alert, AlertTitle, AlertDescription }
|
||||||
|
|
@ -2,48 +2,173 @@
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useParams, useNavigate } from "react-router-dom";
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import { getMigrantById } from "../services/migrantService";
|
import apiService from "../services/apiService";
|
||||||
import type { MigrantProfile } from "../types/migrant";
|
import type { MigrantProfile } from "../types/migrant";
|
||||||
import MigrantProfileComponent from "../components/MigrantProfileComponent";
|
import MigrantProfileComponent from "../components/MigrantProfileComponent";
|
||||||
import LoadingSpinner from "../components/LoadingSpinner";
|
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
|
||||||
|
import { AlertCircle, Loader2 } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
// Helper function to calculate age at migration
|
||||||
|
const calculateAgeAtMigration = (birthDate: string | null | undefined, migrationDate: string | null | undefined): number => {
|
||||||
|
if (!birthDate || !migrationDate) return 0;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const normalizedBirthDate = birthDate.includes('.') ? birthDate.split('.')[0] + 'Z' : birthDate;
|
||||||
|
const normalizedMigrationDate = migrationDate.includes('.') ? migrationDate.split('.')[0] + 'Z' : migrationDate;
|
||||||
|
|
||||||
|
const birthYear = new Date(normalizedBirthDate).getFullYear();
|
||||||
|
const migrationYear = new Date(normalizedMigrationDate).getFullYear();
|
||||||
|
|
||||||
|
// Simple year difference calculation
|
||||||
|
const age = migrationYear - birthYear;
|
||||||
|
|
||||||
|
return age >= 0 ? age : 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error calculating age: birth=${birthDate}, migration=${migrationDate}`, error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const MigrantProfilePage = () => {
|
const MigrantProfilePage = () => {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [migrant, setMigrant] = useState<MigrantProfile | null>(null);
|
const [migrant, setMigrant] = useState<MigrantProfile | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [retryCount, setRetryCount] = useState(0);
|
||||||
|
|
||||||
|
// Fetch migrant data when component mounts or ID changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchMigrant = async () => {
|
const fetchMigrantData = async () => {
|
||||||
|
// Reset state when ID changes
|
||||||
|
setLoading(true);
|
||||||
|
setMigrant(null);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
setError('Missing migrant ID');
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (id) {
|
// Fetch migrant data from the backend using apiService
|
||||||
const data = await getMigrantById(id);
|
const data = await apiService.getRecordById(id);
|
||||||
|
|
||||||
if (data) {
|
if (data) {
|
||||||
setMigrant(data);
|
// Data successfully retrieved - convert API data to MigrantProfile
|
||||||
|
const migrantProfile: MigrantProfile = {
|
||||||
|
id: data.person_id || id,
|
||||||
|
firstName: data.christian_name || '',
|
||||||
|
lastName: data.surname || '',
|
||||||
|
middleName: '',
|
||||||
|
birthDate: data.date_of_birth || '',
|
||||||
|
birthPlace: data.place_of_birth || '',
|
||||||
|
ageAtMigration: calculateAgeAtMigration(data.date_of_birth, data.migration?.date_of_arrival_nt),
|
||||||
|
yearOfArrival: data.migration?.date_of_arrival_nt ?
|
||||||
|
new Date(data.migration.date_of_arrival_nt).getFullYear() : 0,
|
||||||
|
regionOfOrigin: data.place_of_birth || 'Unknown',
|
||||||
|
settlementLocation: data.residence?.town_or_city || 'Unknown',
|
||||||
|
occupation: data.occupation || 'Unknown',
|
||||||
|
deathDate: data.date_of_death || '',
|
||||||
|
deathPlace: data.residence?.home_at_death || '',
|
||||||
|
mainPhoto: '', // No photo URL in the API response
|
||||||
|
biography: data.additional_notes || '',
|
||||||
|
photos: [],
|
||||||
|
relatedMigrants: []
|
||||||
|
};
|
||||||
|
|
||||||
|
setMigrant(migrantProfile);
|
||||||
|
setError(null);
|
||||||
} else {
|
} else {
|
||||||
// Migrant not found, redirect to 404
|
// No data found for this ID
|
||||||
navigate("/not-found");
|
setMigrant(null);
|
||||||
|
setError(`No migrant found with ID: ${id}`);
|
||||||
}
|
}
|
||||||
}
|
} catch (error: any) {
|
||||||
} catch (error) {
|
// Handle API errors
|
||||||
console.error("Error fetching migrant:", error);
|
const errorMessage =
|
||||||
navigate("/not-found");
|
error.response?.data?.message ||
|
||||||
|
error.message ||
|
||||||
|
'An unexpected error occurred';
|
||||||
|
|
||||||
|
console.error('Error fetching migrant:', errorMessage);
|
||||||
|
setError(`Failed to load migrant data: ${errorMessage}`);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMigrant();
|
fetchMigrantData();
|
||||||
}, [id, navigate]);
|
}, [id, retryCount]); // retryCount allows manual retries
|
||||||
|
|
||||||
|
// Handle retry button click
|
||||||
|
const handleRetry = () => {
|
||||||
|
setRetryCount(prev => prev + 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Loading state
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <LoadingSpinner />;
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-[60vh] p-8">
|
||||||
|
<Loader2 className="h-12 w-12 animate-spin text-primary mb-4" />
|
||||||
|
<h2 className="text-xl font-medium mb-2">Loading migrant profile...</h2>
|
||||||
|
<p className="text-gray-500">Retrieving data from the database</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Error state
|
||||||
|
if (error) {
|
||||||
|
// Check if the error is specifically about not finding a migrant
|
||||||
|
const isNotFoundError = error.includes('No migrant found');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-3xl mx-auto p-8">
|
||||||
|
<Alert variant={isNotFoundError ? "default" : "destructive"} className={`mb-6 ${isNotFoundError ? 'border-amber-500' : ''}`}>
|
||||||
|
<AlertCircle className={`h-5 w-5 ${isNotFoundError ? 'text-amber-500' : ''}`} />
|
||||||
|
<AlertTitle>{isNotFoundError ? "Migrant Not Found" : "Error"}</AlertTitle>
|
||||||
|
<AlertDescription>{error}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center mt-8 mb-4">
|
||||||
|
{isNotFoundError && (
|
||||||
|
<div className="text-center mb-6">
|
||||||
|
<p className="text-lg mb-4">The migrant profile you're looking for could not be found in our database.</p>
|
||||||
|
<p className="text-gray-600">This might be because:</p>
|
||||||
|
<ul className="list-disc list-inside text-gray-600 mt-2 mb-4">
|
||||||
|
<li>The ID provided is incorrect</li>
|
||||||
|
<li>The record has been removed from the database</li>
|
||||||
|
<li>The record hasn't been added to the database yet</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-4 justify-center">
|
||||||
|
<Button onClick={() => navigate(-1)}>Go Back</Button>
|
||||||
|
{!isNotFoundError && (
|
||||||
|
<Button variant="outline" onClick={handleRetry}>Retry</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="outline" onClick={() => window.location.href = '/search-test'}>Search Again</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// No data state (should be caught by error state, but just in case)
|
||||||
if (!migrant) {
|
if (!migrant) {
|
||||||
return null; // This should not happen as we redirect to 404 if migrant is null
|
return (
|
||||||
|
<div className="max-w-3xl mx-auto p-8 text-center">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Migrant Profile Not Found</h2>
|
||||||
|
<p className="mb-6">The migrant profile you're looking for could not be found.</p>
|
||||||
|
<Button onClick={() => navigate(-1)}>Return to Previous Page</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render migrant profile when data is loaded successfully
|
||||||
return <MigrantProfileComponent migrant={migrant} />;
|
return <MigrantProfileComponent migrant={migrant} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import type { SearchResult, SearchParams } from "../types/search";
|
||||||
|
|
||||||
|
// Load base URL from environment
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
|
||||||
|
// Create Axios instance
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request interceptor for logging
|
||||||
|
api.interceptors.request.use((request) => {
|
||||||
|
const fullUrl = `${request.baseURL || ""}${request.url || ""}`;
|
||||||
|
console.log("[API] Request URL:", fullUrl);
|
||||||
|
|
||||||
|
if (request.params) {
|
||||||
|
console.log("[API] Params:", request.params);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.url?.includes("/migrants/")) {
|
||||||
|
const id = request.url.split("/").pop();
|
||||||
|
console.log("[API] Fetching migrant by ID:", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return request;
|
||||||
|
});
|
||||||
|
|
||||||
|
class ApiService {
|
||||||
|
/**
|
||||||
|
* Fetch a single migrant record by ID
|
||||||
|
*/
|
||||||
|
async getRecordById(id: string): Promise<SearchResult> {
|
||||||
|
const res = await api.get(`/api/migrants/${id}`);
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search for people based on filters.
|
||||||
|
* Filters out empty and "all" values.
|
||||||
|
*/
|
||||||
|
async searchPeople(params: SearchParams): Promise<SearchResult[]> {
|
||||||
|
const filtered = Object.fromEntries(
|
||||||
|
Object.entries(params)
|
||||||
|
.filter(([_, value]) => value && value !== "all")
|
||||||
|
.map(([key, value]) => [key, value.toString()])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (Object.keys(filtered).length === 0) return [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.get("/api/persons/search", {
|
||||||
|
params: { ...filtered, exactMatch: "true" },
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.data.success && res.data.data ? res.data.data.data : [];
|
||||||
|
} catch (err) {
|
||||||
|
const message =
|
||||||
|
err instanceof Error ? err.message : "Unknown error during search";
|
||||||
|
throw new Error(`[API] Failed to search people: ${message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ApiService();
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
import type { MigrantProfile } from "../types/migrant"
|
|
||||||
import type { SearchParams, SearchResult } from "../types/search"
|
|
||||||
|
|
||||||
// Mock data for demonstration purposes
|
|
||||||
// In a real application, this would be fetched from an API
|
|
||||||
const migrants: MigrantProfile[] = [
|
|
||||||
{
|
|
||||||
id: "1",
|
|
||||||
firstName: "Antonio",
|
|
||||||
lastName: "Rossi",
|
|
||||||
middleName: "Giuseppe",
|
|
||||||
birthDate: "January 12, 1861",
|
|
||||||
birthPlace: "Palermo, Sicily",
|
|
||||||
ageAtMigration: 24,
|
|
||||||
yearOfArrival: 1885,
|
|
||||||
regionOfOrigin: "Sicily",
|
|
||||||
settlementLocation: "Darwin",
|
|
||||||
occupation: "Fisherman",
|
|
||||||
deathDate: "March 3, 1942",
|
|
||||||
deathPlace: "Darwin, Northern Territory",
|
|
||||||
mainPhoto: "/placeholder.jpg",
|
|
||||||
biography: `Antonio Giuseppe Rossi was born in Palermo, Sicily, in 1861 to a family of fishermen. Growing up along the Mediterranean coast, he learned the fishing trade from his father and grandfather, developing skills that would later serve him well in Australia.\n
|
|
||||||
In 1885, at the age of 24, Antonio left Italy seeking new opportunities. The fishing industry in Sicily was becoming increasingly competitive, and stories of Australia's abundant waters and opportunities had reached Italian shores. He arrived in Darwin with little more than his fishing knowledge and a determination to succeed.\n
|
|
||||||
Antonio quickly established himself in Darwin's small but growing fishing community. He was known for his innovative fishing techniques and his ability to predict weather patterns. Within five years of his arrival, he had saved enough money to purchase his own boat, which he named "Sicilia" in honor of his homeland.\n
|
|
||||||
In 1890, Antonio married Maria Bianchi, a fellow Italian immigrant from Calabria. Together they had six children, all of whom grew up helping with the family fishing business. Antonio and Maria were instrumental in establishing Darwin's Italian community, often hosting gatherings that celebrated Italian culture and traditions.\n
|
|
||||||
During World War II, Antonio, then in his eighties, witnessed the bombing of Darwin. Despite his age, he helped in the evacuation efforts, using his boat to transport people to safety. He passed away shortly after, in March 1942, leaving behind a legacy as one of Darwin's pioneering Italian settlers.`,
|
|
||||||
photos: [
|
|
||||||
{
|
|
||||||
url: "/placeholder.jpg",
|
|
||||||
caption: "Antonio on his fishing boat 'Sicilia'",
|
|
||||||
year: 1895,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "/placeholder.jpg",
|
|
||||||
caption: "Antonio and Maria on their wedding day",
|
|
||||||
year: 1890,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
url: "/placeholder.jpg",
|
|
||||||
caption: "The Rossi family outside their Darwin home",
|
|
||||||
year: 1910,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
relatedMigrants: [
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
firstName: "Maria",
|
|
||||||
lastName: "Bianchi",
|
|
||||||
relationship: "Wife",
|
|
||||||
photoUrl: "/placeholder.jpg",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "2",
|
|
||||||
firstName: "Maria",
|
|
||||||
lastName: "Bianchi",
|
|
||||||
ageAtMigration: 19,
|
|
||||||
yearOfArrival: 1892,
|
|
||||||
regionOfOrigin: "Calabria",
|
|
||||||
settlementLocation: "Alice Springs",
|
|
||||||
occupation: "Seamstress",
|
|
||||||
mainPhoto: "/placeholder.jpg",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "3",
|
|
||||||
firstName: "Giuseppe",
|
|
||||||
lastName: "Verdi",
|
|
||||||
ageAtMigration: 32,
|
|
||||||
yearOfArrival: 1901,
|
|
||||||
regionOfOrigin: "Veneto",
|
|
||||||
settlementLocation: "Katherine",
|
|
||||||
occupation: "Farmer",
|
|
||||||
mainPhoto: "/placeholder.jpg",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
export const searchMigrants = async (params: SearchParams): Promise<SearchResult[]> => {
|
|
||||||
// In a real application, this would call an API endpoint
|
|
||||||
// For demonstration, we'll filter the mock data based on the search parameters
|
|
||||||
|
|
||||||
let results = migrants.map((migrant) => ({
|
|
||||||
id: migrant.id,
|
|
||||||
firstName: migrant.firstName,
|
|
||||||
lastName: migrant.lastName,
|
|
||||||
ageAtMigration: migrant.ageAtMigration,
|
|
||||||
yearOfArrival: migrant.yearOfArrival,
|
|
||||||
regionOfOrigin: migrant.regionOfOrigin,
|
|
||||||
settlementLocation: migrant.settlementLocation,
|
|
||||||
photoUrl: migrant.mainPhoto,
|
|
||||||
}))
|
|
||||||
|
|
||||||
// Filter by first name
|
|
||||||
if (params.firstName) {
|
|
||||||
results = results.filter((migrant) =>
|
|
||||||
migrant.firstName.toLowerCase().includes(params.firstName.toString().toLowerCase()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by last name
|
|
||||||
if (params.lastName) {
|
|
||||||
results = results.filter((migrant) =>
|
|
||||||
migrant.lastName.toLowerCase().includes(params.lastName.toString().toLowerCase()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by age at migration
|
|
||||||
if (params.ageAtMigration) {
|
|
||||||
results = results.filter((migrant) => migrant.ageAtMigration === Number(params.ageAtMigration))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by year of arrival
|
|
||||||
if (params.yearOfArrival) {
|
|
||||||
results = results.filter((migrant) => migrant.yearOfArrival === Number(params.yearOfArrival))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by region of origin
|
|
||||||
if (params.regionOfOrigin && params.regionOfOrigin !== "all") {
|
|
||||||
results = results.filter((migrant) => migrant.regionOfOrigin === params.regionOfOrigin)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by settlement location
|
|
||||||
if (params.settlementLocation && params.settlementLocation !== "all") {
|
|
||||||
results = results.filter((migrant) => migrant.settlementLocation === params.settlementLocation)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simulate API delay
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getMigrantById = async (id: string): Promise<MigrantProfile | null> => {
|
|
||||||
// In a real application, this would call an API endpoint
|
|
||||||
// For demonstration, we'll find the migrant in our mock data
|
|
||||||
const migrant = migrants.find((m) => m.id === id)
|
|
||||||
|
|
||||||
// Simulate API delay
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300))
|
|
||||||
|
|
||||||
return migrant || null
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import axios from "axios";
|
||||||
|
import type { SearchParams, SearchResult } from "@/types/search";
|
||||||
|
import type { MigrantProfile } from "@/types/migrant";
|
||||||
|
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: API_BASE_URL,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
class ApiService {
|
||||||
|
async getRecords(): Promise<SearchResult[]> {
|
||||||
|
return api.get('/api/persons/search').then((response) => response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
export const apiService = new ApiService();
|
||||||
|
|
||||||
|
export const searchPeople = async (params: SearchParams): Promise<SearchResult[]> => {
|
||||||
|
const query = new URLSearchParams();
|
||||||
|
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value && value !== "all") {
|
||||||
|
query.append(key, value.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.get(
|
||||||
|
`/api/persons/search?${query.toString()}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const responseData = response.data;
|
||||||
|
if (responseData.success && responseData.data) {
|
||||||
|
return responseData.data.data.map((person: any) => ({
|
||||||
|
id: person.person_id || person.id,
|
||||||
|
firstName: person.christian_name,
|
||||||
|
lastName: person.surname,
|
||||||
|
yearOfArrival: person.migration?.date_of_arrival_nt ?
|
||||||
|
new Date(person.migration.date_of_arrival_nt).getFullYear() : 'Unknown',
|
||||||
|
ageAtMigration: person.age_at_migration || 'Unknown',
|
||||||
|
regionOfOrigin: person.place_of_birth || 'Unknown',
|
||||||
|
settlementLocation: person.residence?.suburb || 'Unknown',
|
||||||
|
occupation: person.occupation || 'Unknown',
|
||||||
|
photoUrl: person.photo_url || null
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error searching people:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves detailed profile information for a specific migrant by ID
|
||||||
|
* @param id - The migrant's unique identifier
|
||||||
|
* @returns Promise containing the migrant's complete profile or null if not found
|
||||||
|
*/
|
||||||
|
export const getMigrantById = async (id: string): Promise<MigrantProfile | null> => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(`Making API request to: ${API_BASE_URL}/api/migrants/${id}`);
|
||||||
|
|
||||||
|
const response = await axios.get(
|
||||||
|
`${API_BASE_URL}/api/migrants/${id}`
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('API Response:', response.status, response.statusText);
|
||||||
|
console.log('Response data:', response.data);
|
||||||
|
|
||||||
|
const responseData = response.data;
|
||||||
|
|
||||||
|
if (responseData.success && responseData.data) {
|
||||||
|
console.log('Successfully parsed response data');
|
||||||
|
const person = responseData.data;
|
||||||
|
return {
|
||||||
|
id: person.person_id || person.id,
|
||||||
|
firstName: person.christian_name,
|
||||||
|
lastName: person.surname,
|
||||||
|
middleName: person.middle_name,
|
||||||
|
birthDate: person.date_of_birth
|
||||||
|
? new Date(person.date_of_birth.split('.')[0] + 'Z').toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})
|
||||||
|
: 'Unknown',
|
||||||
|
birthPlace: person.place_of_birth,
|
||||||
|
ageAtMigration: person.age_at_migration ? Number(person.age_at_migration) : 0,
|
||||||
|
yearOfArrival: person.migration?.date_of_arrival_nt ?
|
||||||
|
new Date(person.migration.date_of_arrival_nt).getFullYear() : 0,
|
||||||
|
regionOfOrigin: person.place_of_birth || 'Unknown',
|
||||||
|
settlementLocation: person.residence?.suburb || 'Unknown',
|
||||||
|
occupation: person.occupation || 'Unknown',
|
||||||
|
deathDate: person.date_of_death
|
||||||
|
? new Date(person.date_of_death.split('.')[0] + 'Z').toLocaleDateString('en-US', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric',
|
||||||
|
})
|
||||||
|
: 'Unknown',
|
||||||
|
deathPlace: person.residence?.home_at_death || 'Unknown',
|
||||||
|
mainPhoto: person.photo_url || null,
|
||||||
|
biography: person.biography || '',
|
||||||
|
photos: person.photos || [],
|
||||||
|
relatedMigrants: person.related_persons || []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
console.error('Axios error in getMigrantById:', {
|
||||||
|
status: error.response?.status,
|
||||||
|
statusText: error.response?.statusText,
|
||||||
|
url: error.config?.url,
|
||||||
|
data: error.response?.data
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the error is a 404, we can return null to indicate no migrant found
|
||||||
|
if (error.response?.status === 404) {
|
||||||
|
console.log('Migrant not found (404 response)');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Non-Axios error fetching migrant profile:', error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -1,19 +1,84 @@
|
||||||
export interface SearchParams {
|
export interface SearchParams {
|
||||||
firstName: string
|
firstName?: string;
|
||||||
lastName: string
|
lastName?: string;
|
||||||
ageAtMigration: string | number
|
ageAtMigration?: string;
|
||||||
yearOfArrival: string | number
|
yearOfArrival?: string;
|
||||||
regionOfOrigin: string
|
regionOfOrigin?: string;
|
||||||
settlementLocation: string
|
settlementLocation?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SearchResult {
|
// Pagination metadata structure
|
||||||
id: string
|
export interface Pagination {
|
||||||
firstName: string
|
total: number;
|
||||||
lastName: string
|
currentPage: number;
|
||||||
ageAtMigration: number
|
totalPages: number;
|
||||||
yearOfArrival: number
|
perPage: number;
|
||||||
regionOfOrigin: string
|
}
|
||||||
settlementLocation: string
|
|
||||||
photoUrl?: string
|
// Migration details for a person
|
||||||
|
export interface Migration {
|
||||||
|
date_of_arrival_aus?: string;
|
||||||
|
date_of_arrival_nt?: string;
|
||||||
|
arrival_period?: string;
|
||||||
|
data_source?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Naturalization details for a person
|
||||||
|
export interface Naturalization {
|
||||||
|
certificate_number?: string;
|
||||||
|
date_of_naturalization?: string;
|
||||||
|
previous_nationality?: string;
|
||||||
|
place_of_naturalization?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Residence details for a person
|
||||||
|
export interface Residence {
|
||||||
|
town_or_city?: string;
|
||||||
|
home_at_death?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Family details for a person
|
||||||
|
export interface Family {
|
||||||
|
spouse_name?: string;
|
||||||
|
spouse_origin?: string;
|
||||||
|
number_of_children?: number;
|
||||||
|
names_of_children?: string;
|
||||||
|
additional_notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internment details for a person
|
||||||
|
export interface Internment {
|
||||||
|
was_interned: boolean;
|
||||||
|
camp_name?: string;
|
||||||
|
date_of_internment?: string;
|
||||||
|
date_of_release?: string;
|
||||||
|
additional_notes?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A single search result representing a person record
|
||||||
|
export interface SearchResult {
|
||||||
|
person_id: string;
|
||||||
|
surname: string;
|
||||||
|
christian_name: string;
|
||||||
|
full_name: string;
|
||||||
|
date_of_birth?: string;
|
||||||
|
place_of_birth?: string;
|
||||||
|
date_of_death?: string;
|
||||||
|
occupation?: string;
|
||||||
|
additional_notes?: string;
|
||||||
|
reference?: string;
|
||||||
|
id_card_no?: string;
|
||||||
|
migration: Migration;
|
||||||
|
naturalization: Naturalization;
|
||||||
|
residence: Residence;
|
||||||
|
family: Family;
|
||||||
|
internment: Internment;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The overall search response from the API
|
||||||
|
export interface SearchResponse {
|
||||||
|
data: SearchResult[];
|
||||||
|
pagination: Pagination;
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
@ -11,4 +11,14 @@ export default defineConfig({
|
||||||
"@": path.resolve(__dirname, "./src"),
|
"@": path.resolve(__dirname, "./src"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
"/api": {
|
||||||
|
target: "http://localhost:8000",
|
||||||
|
changeOrigin: true,
|
||||||
|
// Don't rewrite the path since Laravel expects /api prefix
|
||||||
|
// rewrite: (path) => path.replace(/^\/api/, ""),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue