This commit is contained in:
commit
73db59a56e
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default tseslint.config({
|
||||||
|
extends: [
|
||||||
|
// Remove ...tseslint.configs.recommended and replace with this
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
...tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
// other options...
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default tseslint.config({
|
||||||
|
plugins: {
|
||||||
|
// Add the react-x and react-dom plugins
|
||||||
|
'react-x': reactX,
|
||||||
|
'react-dom': reactDom,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// other rules...
|
||||||
|
// Enable its recommended typescript rules
|
||||||
|
...reactX.configs['recommended-typescript'].rules,
|
||||||
|
...reactDom.configs.recommended.rules,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": false,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{ ignores: ['dist'] },
|
||||||
|
{
|
||||||
|
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
'react-hooks': reactHooks,
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
...reactHooks.configs.recommended.rules,
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{ allowConstantExport: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React + TS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"name": "italian-migrants-nt",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-label": "^2.1.6",
|
||||||
|
"@radix-ui/react-select": "^2.2.4",
|
||||||
|
"@radix-ui/react-slot": "^1.2.2",
|
||||||
|
"@tailwindcss/vite": "^4.1.6",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"framer-motion": "^12.11.0",
|
||||||
|
"lucide-react": "^0.510.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^7.6.0",
|
||||||
|
"tailwind-merge": "^3.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.25.0",
|
||||||
|
"@types/node": "^22.15.17",
|
||||||
|
"@types/react": "^19.1.2",
|
||||||
|
"@types/react-dom": "^19.1.2",
|
||||||
|
"@vitejs/plugin-react": "^4.4.1",
|
||||||
|
"autoprefixer": "^10.4.21",
|
||||||
|
"eslint": "^9.25.0",
|
||||||
|
"eslint-plugin-react-hooks": "^5.2.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"postcss": "^8.5.3",
|
||||||
|
"tailwindcss": "^4.1.6",
|
||||||
|
"tw-animate-css": "^1.2.9",
|
||||||
|
"typescript": "~5.8.3",
|
||||||
|
"typescript-eslint": "^8.30.1",
|
||||||
|
"vite": "^6.3.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|
||||||
|
After Width: | Height: | Size: 3.2 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||||
|
import HomePage from "./pages/HomePage";
|
||||||
|
import MigrantProfilePage from "./pages/MigrantProfilePage";
|
||||||
|
import NotFoundPage from "./pages/NotFoundPage";
|
||||||
|
import "./App.css";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<HomePage />} />
|
||||||
|
<Route path="/migrant/:id" element={<MigrantProfilePage />} />
|
||||||
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import AnimatedImage from "@/components/ui/animated-image";
|
||||||
|
|
||||||
|
export default function HeroSection() {
|
||||||
|
return (
|
||||||
|
<section className="relative w-full h-[70vh] md:h-[80vh] overflow-hidden">
|
||||||
|
<div className="absolute inset-0 bg-black/60 z-10" />
|
||||||
|
<div className="relative h-full w-full">
|
||||||
|
<AnimatedImage
|
||||||
|
src="https://globalboston.bc.edu/wp-content/uploads/2016/07/06_01_012688.jpg"
|
||||||
|
alt="Historical image of Italian migrants in the Northern Territory"
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="absolute inset-0 z-20 flex flex-col items-center justify-center text-center px-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.2 }}
|
||||||
|
className="max-w-4xl"
|
||||||
|
>
|
||||||
|
{/* Top 4 bars */}
|
||||||
|
<div className="flex items-center justify-center mb-6 space-x-1">
|
||||||
|
<div className="h-1 w-12 bg-green-600" />
|
||||||
|
<div className="h-1 w-12 bg-white" />
|
||||||
|
<div className="h-1 w-12 bg-red-600" />
|
||||||
|
<div className="h-1 w-12 bg-gray-400" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="text-4xl md:text-6xl font-bold text-white mb-4 font-serif">
|
||||||
|
Italian Migration to the Northern Territory
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl text-white max-w-3xl italic">
|
||||||
|
Exploring the rich history and cultural legacy of Italian immigrants
|
||||||
|
in Australia's Northern Territory
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Bottom 4 bars */}
|
||||||
|
<div className="flex items-center justify-center mt-6 space-x-1">
|
||||||
|
<div className="h-1 w-12 bg-green-600" />
|
||||||
|
<div className="h-1 w-12 bg-white" />
|
||||||
|
<div className="h-1 w-12 bg-red-600" />
|
||||||
|
<div className="h-1 w-12 bg-gray-400" />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 h-16 bg-gradient-to-t from-black to-transparent z-10" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
interface HistoricalContextProps {
|
||||||
|
year: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HistoricalContext = ({ year }: HistoricalContextProps) => {
|
||||||
|
// Function to get historical context based on the year
|
||||||
|
const getHistoricalContext = (year: number) => {
|
||||||
|
if (year < 1880) {
|
||||||
|
return {
|
||||||
|
period: "Early Settlement Period",
|
||||||
|
description:
|
||||||
|
"The earliest Italian migrants to the Northern Territory arrived during this period, primarily as individuals seeking opportunity in a new land. They often worked in mining, agriculture, or as merchants.",
|
||||||
|
events: [
|
||||||
|
"Early European settlement in the Northern Territory",
|
||||||
|
"Gold rushes in various parts of Australia",
|
||||||
|
"Establishment of Darwin (then called Palmerston)",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else if (year < 1900) {
|
||||||
|
return {
|
||||||
|
period: "Late 19th Century",
|
||||||
|
description:
|
||||||
|
"This period saw increased Italian migration to Australia, with many arriving to work in agriculture, fishing, and construction. The Northern Territory's tropical climate was familiar to those from southern Italy.",
|
||||||
|
events: [
|
||||||
|
"Construction of the Overland Telegraph Line",
|
||||||
|
"Expansion of the pastoral industry",
|
||||||
|
"Growth of Darwin as a port city",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else if (year < 1920) {
|
||||||
|
return {
|
||||||
|
period: "Federation and Early 20th Century",
|
||||||
|
description:
|
||||||
|
"Following Australian Federation in 1901, immigration policies became more restrictive, but Italians continued to arrive. Many came to join family members already established in the Territory.",
|
||||||
|
events: [
|
||||||
|
"Federation of Australia (1901)",
|
||||||
|
"World War I (1914-1918)",
|
||||||
|
"Development of the pearling industry",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else if (year < 1940) {
|
||||||
|
return {
|
||||||
|
period: "Interwar Period",
|
||||||
|
description:
|
||||||
|
"The interwar period saw continued Italian migration, with many escaping economic hardship and the rise of fascism in Italy. Chain migration became common, with established migrants sponsoring relatives.",
|
||||||
|
events: [
|
||||||
|
"Great Depression (1929-1933)",
|
||||||
|
"Rise of fascism in Italy",
|
||||||
|
"Expansion of agriculture in the Northern Territory",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else if (year < 1960) {
|
||||||
|
return {
|
||||||
|
period: "Post-World War II",
|
||||||
|
description:
|
||||||
|
"After World War II, Australia actively encouraged European migration. Many Italians came to Australia as part of assisted migration schemes, seeking to escape the devastation of post-war Italy.",
|
||||||
|
events: [
|
||||||
|
"World War II (1939-1945)",
|
||||||
|
"Beginning of Australia's 'Populate or Perish' policy",
|
||||||
|
"Bombing of Darwin (1942)",
|
||||||
|
"Reconstruction of Darwin",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else if (year < 1980) {
|
||||||
|
return {
|
||||||
|
period: "Modern Migration",
|
||||||
|
description:
|
||||||
|
"This period saw continued Italian migration, though at a slower pace than previous decades. Many migrants were skilled workers or came to join established family networks.",
|
||||||
|
events: [
|
||||||
|
"End of the White Australia Policy",
|
||||||
|
"Cyclone Tracy devastates Darwin (1974)",
|
||||||
|
"Northern Territory granted self-government (1978)",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
period: "Contemporary Era",
|
||||||
|
description:
|
||||||
|
"Recent Italian migration to the Northern Territory has been limited, with most new arrivals being skilled professionals or family members of established migrants. The Italian community is now largely Australian-born descendants of earlier migrants.",
|
||||||
|
events: [
|
||||||
|
"Multiculturalism becomes official policy",
|
||||||
|
"Growth of tourism in the Northern Territory",
|
||||||
|
"Development of Darwin as a gateway to Asia",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const context = getHistoricalContext(year);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="p-6">
|
||||||
|
<h2 className="text-xl font-bold mb-2">Historical Context</h2>
|
||||||
|
<h3 className="font-medium text-lg mb-2">{context.period}</h3>
|
||||||
|
<p className="text-sm mb-4">{context.description}</p>
|
||||||
|
<h4 className="font-medium text-sm mb-1">Key Events:</h4>
|
||||||
|
<ul className="text-sm list-disc pl-5 space-y-1">
|
||||||
|
{context.events.map((event, index) => (
|
||||||
|
<li key={index}>{event}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HistoricalContext;
|
||||||
|
|
@ -0,0 +1,83 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import AnimatedImage from "@/components/ui/animated-image";
|
||||||
|
|
||||||
|
export default function IntroSection() {
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const itemVariants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: { opacity: 1, y: 0, transition: { duration: 0.6 } },
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-16 px-4 md:px-8 max-w-6xl mx-auto">
|
||||||
|
<motion.div
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="visible"
|
||||||
|
viewport={{ once: true, margin: "-100px" }}
|
||||||
|
variants={containerVariants}
|
||||||
|
>
|
||||||
|
<motion.div variants={itemVariants} className="text-center mb-12">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-4 font-serif text-gray-800">
|
||||||
|
La Nostra Storia
|
||||||
|
</h2>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="h-1 w-12 bg-green-600" />
|
||||||
|
<div className="h-1 w-12 bg-white" />
|
||||||
|
<div className="h-1 w-12 bg-red-600" />
|
||||||
|
</div>
|
||||||
|
<p className="text-xl mt-4 text-gray-600 italic">Our History</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-2 gap-8 items-center">
|
||||||
|
<motion.div variants={itemVariants} className="space-y-4">
|
||||||
|
<p className="text-lg">
|
||||||
|
The history of Italian migration to the Northern Territory dates
|
||||||
|
back to the late 19th century, when the first Italian settlers
|
||||||
|
arrived seeking new opportunities and a better life.
|
||||||
|
</p>
|
||||||
|
<p className="text-lg">
|
||||||
|
These pioneers played a significant role in shaping the region's
|
||||||
|
development, contributing to industries such as agriculture,
|
||||||
|
fishing, construction, and mining.
|
||||||
|
</p>
|
||||||
|
<p className="text-lg">
|
||||||
|
Many Italian families established deep roots in the Territory,
|
||||||
|
creating vibrant communities that preserved their cultural
|
||||||
|
traditions while embracing their new Australian home.
|
||||||
|
</p>
|
||||||
|
<div className="pt-4 border-t border-gray-200">
|
||||||
|
<blockquote className="italic text-gray-700 pl-4 border-l-4 border-red-500">
|
||||||
|
"They brought with them not only their skills and work ethic,
|
||||||
|
but also their rich cultural heritage, culinary traditions, and
|
||||||
|
strong family values."
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div variants={itemVariants}>
|
||||||
|
<div className="relative h-[300px] md:h-[400px] rounded-lg overflow-hidden shadow-lg border-8 border-white">
|
||||||
|
<AnimatedImage
|
||||||
|
src="https://www.lagazzettaitaliana.com/media/k2/items/cache/c62caf210f3cc558e679c3751b1975e6_XL.jpg"
|
||||||
|
alt="Italian family in the Northern Territory"
|
||||||
|
fill
|
||||||
|
/>
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 bg-black/60 text-white p-3 text-sm">
|
||||||
|
Italian family settling in the Northern Territory, circa 1920s
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
const LoadingSpinner = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-primary-color"></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LoadingSpinner
|
||||||
|
|
@ -0,0 +1,146 @@
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import type { MigrantProfile } from "../types/migrant"
|
||||||
|
import PhotoGallery from "./PhotoGallery"
|
||||||
|
import RelatedMigrants from "./RelatedMigrants"
|
||||||
|
import HistoricalContext from "./HistoricalContext"
|
||||||
|
|
||||||
|
interface MigrantProfileComponentProps {
|
||||||
|
migrant: MigrantProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
const MigrantProfileComponent = ({ migrant }: MigrantProfileComponentProps) => {
|
||||||
|
return (
|
||||||
|
<main className="min-h-screen bg-gray-50 pb-16">
|
||||||
|
{/* Hero section with main photo */}
|
||||||
|
<div className="relative w-full h-[40vh] md:h-[50vh] overflow-hidden bg-gray-900">
|
||||||
|
<img
|
||||||
|
src={migrant.mainPhoto || "/placeholder.jpg"}
|
||||||
|
alt={`${migrant.firstName} ${migrant.lastName}`}
|
||||||
|
className="w-full h-full object-cover opacity-80"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent" />
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 p-6 md:p-10 text-white">
|
||||||
|
<h1 className="text-3xl md:text-5xl font-bold mb-2">
|
||||||
|
{migrant.firstName} {migrant.lastName}
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl md:text-2xl opacity-90">
|
||||||
|
{migrant.yearOfArrival} • {migrant.regionOfOrigin}, Italy → {migrant.settlementLocation}, NT
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Back button */}
|
||||||
|
<div className="max-w-6xl mx-auto px-4 md:px-8 mt-6">
|
||||||
|
<Link to="/" className="btn btn-outline flex items-center gap-2 mb-6">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
Back to Search
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
|
{/* Main content - 2/3 width on desktop */}
|
||||||
|
<div className="lg:col-span-2 space-y-8">
|
||||||
|
{/* Biographical information */}
|
||||||
|
<div className="card">
|
||||||
|
<div className="p-6">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Biographical Information</h2>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-8">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Full Name</h3>
|
||||||
|
<p className="text-lg">
|
||||||
|
{migrant.firstName} {migrant.middleName ? migrant.middleName + " " : ""}
|
||||||
|
{migrant.lastName}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Birth Date</h3>
|
||||||
|
<p className="text-lg">{migrant.birthDate || "Unknown"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Birth Place</h3>
|
||||||
|
<p className="text-lg">{migrant.birthPlace || "Unknown"}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Age at Migration</h3>
|
||||||
|
<p className="text-lg">{migrant.ageAtMigration} years</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Year of Arrival</h3>
|
||||||
|
<p className="text-lg">{migrant.yearOfArrival}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Region of Origin</h3>
|
||||||
|
<p className="text-lg">{migrant.regionOfOrigin}, Italy</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Settlement Location</h3>
|
||||||
|
<p className="text-lg">{migrant.settlementLocation}, NT</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Occupation</h3>
|
||||||
|
<p className="text-lg">{migrant.occupation || "Unknown"}</p>
|
||||||
|
</div>
|
||||||
|
{migrant.deathDate && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Date of Death</h3>
|
||||||
|
<p className="text-lg">{migrant.deathDate}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-medium text-gray-500">Place of Death</h3>
|
||||||
|
<p className="text-lg">{migrant.deathPlace || "Unknown"}</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Life story */}
|
||||||
|
{migrant.biography && (
|
||||||
|
<div className="card">
|
||||||
|
<div className="p-6">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Life Story</h2>
|
||||||
|
<div className="prose max-w-none">
|
||||||
|
{migrant.biography.split("\n").map((paragraph, index) => (
|
||||||
|
<p key={index} className="mb-4">
|
||||||
|
{paragraph}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Photo gallery - only show if there are additional photos */}
|
||||||
|
{migrant.photos && migrant.photos.length > 0 && <PhotoGallery photos={migrant.photos} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Sidebar - 1/3 width on desktop */}
|
||||||
|
<div className="space-y-8">
|
||||||
|
{/* Historical context */}
|
||||||
|
<HistoricalContext year={migrant.yearOfArrival} />
|
||||||
|
|
||||||
|
{/* Related migrants */}
|
||||||
|
{migrant.relatedMigrants && migrant.relatedMigrants.length > 0 && (
|
||||||
|
<RelatedMigrants migrants={migrant.relatedMigrants} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MigrantProfileComponent
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import type { Photo } from "../types/migrant"
|
||||||
|
|
||||||
|
interface PhotoGalleryProps {
|
||||||
|
photos: Photo[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const PhotoGallery = ({ photos }: PhotoGalleryProps) => {
|
||||||
|
const [currentPhotoIndex, setCurrentPhotoIndex] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const openModal = (index: number) => {
|
||||||
|
setCurrentPhotoIndex(index)
|
||||||
|
document.body.style.overflow = "hidden"
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
setCurrentPhotoIndex(null)
|
||||||
|
document.body.style.overflow = "auto"
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePrevious = () => {
|
||||||
|
if (currentPhotoIndex === null) return
|
||||||
|
setCurrentPhotoIndex((prev) => (prev === 0 ? photos.length - 1 : prev - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
if (currentPhotoIndex === null) return
|
||||||
|
setCurrentPhotoIndex((prev) => (prev === photos.length - 1 ? 0 : prev + 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="card">
|
||||||
|
<div className="p-6">
|
||||||
|
<h2 className="text-2xl font-bold mb-4">Photo Gallery</h2>
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
{photos.map((photo, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className="relative h-40 w-full rounded-md overflow-hidden border border-gray-200 hover:opacity-90 transition-opacity"
|
||||||
|
onClick={() => openModal(index)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={photo.url || "/placeholder.jpg"}
|
||||||
|
alt={photo.caption || `Photo ${index + 1}`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modal */}
|
||||||
|
{currentPhotoIndex !== null && (
|
||||||
|
<div className="fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="relative bg-black rounded-lg overflow-hidden max-w-4xl w-full max-h-[90vh]">
|
||||||
|
<button
|
||||||
|
className="absolute top-2 right-2 z-10 text-white bg-black/50 hover:bg-black/70 p-2 rounded-full"
|
||||||
|
onClick={closeModal}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M18 6L6 18M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div className="relative h-[70vh] w-full">
|
||||||
|
<img
|
||||||
|
src={photos[currentPhotoIndex].url || "/placeholder.jpg"}
|
||||||
|
alt={photos[currentPhotoIndex].caption || `Photo ${currentPhotoIndex + 1}`}
|
||||||
|
className="w-full h-full object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{photos.length > 1 && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="absolute left-2 top-1/2 -translate-y-1/2 text-white bg-black/50 hover:bg-black/70 p-2 rounded-full"
|
||||||
|
onClick={handlePrevious}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M15 18l-6-6 6-6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-white bg-black/50 hover:bg-black/70 p-2 rounded-full"
|
||||||
|
onClick={handleNext}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M9 18l6-6-6-6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{photos[currentPhotoIndex].caption && (
|
||||||
|
<div className="absolute bottom-0 left-0 right-0 bg-black/70 text-white p-4">
|
||||||
|
<p>{photos[currentPhotoIndex].caption}</p>
|
||||||
|
{photos[currentPhotoIndex].year && (
|
||||||
|
<p className="text-sm text-gray-300">Year: {photos[currentPhotoIndex].year}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PhotoGallery
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import type { RelatedMigrant } from "../types/migrant"
|
||||||
|
|
||||||
|
interface RelatedMigrantsProps {
|
||||||
|
migrants: RelatedMigrant[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const RelatedMigrants = ({ migrants }: RelatedMigrantsProps) => {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="p-6">
|
||||||
|
<h2 className="text-xl font-bold mb-4">Related Migrants</h2>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{migrants.map((migrant) => (
|
||||||
|
<Link
|
||||||
|
key={migrant.id}
|
||||||
|
to={`/migrant/${migrant.id}`}
|
||||||
|
className="flex items-center gap-3 p-2 rounded-md hover:bg-gray-100 transition-colors"
|
||||||
|
>
|
||||||
|
<div className="relative h-12 w-12 rounded-full overflow-hidden flex-shrink-0">
|
||||||
|
<img
|
||||||
|
src={migrant.photoUrl || "/placeholder.jpg"}
|
||||||
|
alt={`${migrant.firstName} ${migrant.lastName}`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium">
|
||||||
|
{migrant.firstName} {migrant.lastName}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-gray-500">{migrant.relationship}</p>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RelatedMigrants
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, type ChangeEvent, type FormEvent } from "react";
|
||||||
|
import type { SearchParams } from "../types/search";
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
onSearch: (params: SearchParams) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchForm = ({ onSearch }: SearchFormProps) => {
|
||||||
|
const [formData, setFormData] = useState<SearchParams>({
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
ageAtMigration: "",
|
||||||
|
yearOfArrival: "",
|
||||||
|
regionOfOrigin: "all",
|
||||||
|
settlementLocation: "all",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectChange = (e: ChangeEvent<HTMLSelectElement>) => {
|
||||||
|
const { name, value } = e.target;
|
||||||
|
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (e: FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSearch(formData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
setFormData({
|
||||||
|
firstName: "",
|
||||||
|
lastName: "",
|
||||||
|
ageAtMigration: "",
|
||||||
|
yearOfArrival: "",
|
||||||
|
regionOfOrigin: "all",
|
||||||
|
settlementLocation: "all",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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="space-y-2">
|
||||||
|
<label htmlFor="firstName" className="block text-sm font-medium">
|
||||||
|
First Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="firstName"
|
||||||
|
name="firstName"
|
||||||
|
type="text"
|
||||||
|
value={formData.firstName}
|
||||||
|
onChange={handleInputChange}
|
||||||
|
placeholder="Enter first name"
|
||||||
|
className="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label htmlFor="lastName" className="block text-sm font-medium">
|
||||||
|
Last Name
|
||||||
|
</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>
|
||||||
|
<select
|
||||||
|
id="regionOfOrigin"
|
||||||
|
name="regionOfOrigin"
|
||||||
|
value={formData.regionOfOrigin}
|
||||||
|
onChange={handleSelectChange}
|
||||||
|
className="w-full p-2 border border-gray-300 rounded-md"
|
||||||
|
>
|
||||||
|
<option value="all">All Regions</option>
|
||||||
|
{ITALIAN_REGIONS.map((region) => (
|
||||||
|
<option key={region} value={region}>
|
||||||
|
{region}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</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 className="flex flex-wrap gap-4 mt-8 justify-end">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleReset}
|
||||||
|
className="border border-gray-400 text-gray-700 px-4 py-2 rounded hover:bg-gray-100 transition"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition"
|
||||||
|
>
|
||||||
|
Search Records
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SearchForm;
|
||||||
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import type { SearchResult } from "../types/search"
|
||||||
|
import LoadingSpinner from "./LoadingSpinner"
|
||||||
|
|
||||||
|
interface SearchResultsProps {
|
||||||
|
results: SearchResult[]
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const SearchResults = ({ results, isLoading }: SearchResultsProps) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<LoadingSpinner />
|
||||||
|
<p className="mt-4 text-gray-500">Searching records...</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (results.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<h3 className="text-2xl font-semibold mb-4">No Results Found</h3>
|
||||||
|
<p className="text-gray-500">Try adjusting your search criteria to find more records.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="text-2xl font-semibold mb-6">Search Results ({results.length})</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{results.map((person) => (
|
||||||
|
<Link key={person.id} to={`/migrant/${person.id}`} className="block">
|
||||||
|
<div className="card h-full hover:shadow-lg transition-shadow">
|
||||||
|
<div className="relative h-48 w-full">
|
||||||
|
<img
|
||||||
|
src={person.photoUrl || "/placeholder.jpg"}
|
||||||
|
alt={`${person.firstName} ${person.lastName}`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="p-4">
|
||||||
|
<h4 className="text-xl font-semibold">
|
||||||
|
{person.firstName} {person.lastName}
|
||||||
|
</h4>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Arrived {person.yearOfArrival} at age {person.ageAtMigration}
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 space-y-2">
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">From:</span> {person.regionOfOrigin}, Italy
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span className="font-medium">Settled in:</span> {person.settlementLocation}, NT
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchResults
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState } from "react"
|
||||||
|
import SearchForm from "./SearchForm"
|
||||||
|
import SearchResults from "./SearchResults"
|
||||||
|
import type { SearchParams, SearchResult } from "../types/search"
|
||||||
|
import { searchMigrants } from "../services/migrantService"
|
||||||
|
|
||||||
|
const SearchSection = () => {
|
||||||
|
const [searchResults, setSearchResults] = useState<SearchResult[]>([])
|
||||||
|
const [isSearching, setIsSearching] = useState(false)
|
||||||
|
const [hasSearched, setHasSearched] = useState(false)
|
||||||
|
|
||||||
|
const handleSearch = async (params: SearchParams) => {
|
||||||
|
setIsSearching(true)
|
||||||
|
try {
|
||||||
|
const results = await searchMigrants(params)
|
||||||
|
setSearchResults(results)
|
||||||
|
setHasSearched(true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error searching migrants:", error)
|
||||||
|
// In a real application, you would handle this error more gracefully
|
||||||
|
} finally {
|
||||||
|
setIsSearching(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="py-16 px-4 md:px-8 bg-gray-50">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-center">Search Historical Records</h2>
|
||||||
|
<SearchForm onSearch={handleSearch} />
|
||||||
|
{(isSearching || hasSearched) && <SearchResults results={searchResults} isLoading={isSearching} />}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchSection
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
interface AnimatedImageProps {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
className?: string;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
fill?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AnimatedImage({
|
||||||
|
src,
|
||||||
|
alt,
|
||||||
|
className = "",
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fill = false,
|
||||||
|
}: AnimatedImageProps) {
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className={`relative overflow-hidden ${
|
||||||
|
fill ? "h-full w-full" : ""
|
||||||
|
} ${className}`}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: isLoaded ? 1 : 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={src || "/placeholder.svg"}
|
||||||
|
alt={alt}
|
||||||
|
width={!fill ? width : undefined}
|
||||||
|
height={!fill ? height : undefined}
|
||||||
|
className={`object-cover ${fill ? "h-full w-full" : ""}`}
|
||||||
|
onLoad={() => setIsLoaded(true)}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||||
|
outline:
|
||||||
|
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
||||||
|
ghost:
|
||||||
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||||
|
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||||
|
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||||
|
icon: "size-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
function Button({
|
||||||
|
className,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
asChild = false,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"button"> &
|
||||||
|
VariantProps<typeof buttonVariants> & {
|
||||||
|
asChild?: boolean
|
||||||
|
}) {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
data-slot="button"
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card"
|
||||||
|
className={cn(
|
||||||
|
"bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-header"
|
||||||
|
className={cn(
|
||||||
|
"@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-title"
|
||||||
|
className={cn("leading-none font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-description"
|
||||||
|
className={cn("text-muted-foreground text-sm", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-action"
|
||||||
|
className={cn(
|
||||||
|
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-content"
|
||||||
|
className={cn("px-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="card-footer"
|
||||||
|
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Card,
|
||||||
|
CardHeader,
|
||||||
|
CardFooter,
|
||||||
|
CardTitle,
|
||||||
|
CardAction,
|
||||||
|
CardDescription,
|
||||||
|
CardContent,
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
import * as React from "react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
data-slot="input"
|
||||||
|
className={cn(
|
||||||
|
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||||
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||||
|
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Input }
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Label({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<LabelPrimitive.Root
|
||||||
|
data-slot="label"
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Label }
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Select({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||||
|
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectGroup({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||||
|
return <SelectPrimitive.Group data-slot="select-group" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectValue({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||||
|
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectTrigger({
|
||||||
|
className,
|
||||||
|
size = "default",
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||||
|
size?: "sm" | "default"
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
data-slot="select-trigger"
|
||||||
|
data-size={size}
|
||||||
|
className={cn(
|
||||||
|
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDownIcon className="size-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectContent({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
position = "popper",
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
data-slot="select-content"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectScrollUpButton />
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
<SelectScrollDownButton />
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectLabel({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
data-slot="select-label"
|
||||||
|
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectItem({
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
data-slot="select-item"
|
||||||
|
className={cn(
|
||||||
|
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<CheckIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
data-slot="select-separator"
|
||||||
|
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollUpButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollUpButton
|
||||||
|
data-slot="select-scroll-up-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronUpIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollUpButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SelectScrollDownButton({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||||
|
return (
|
||||||
|
<SelectPrimitive.ScrollDownButton
|
||||||
|
data-slot="select-scroll-down-button"
|
||||||
|
className={cn(
|
||||||
|
"flex cursor-default items-center justify-center py-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="size-4" />
|
||||||
|
</SelectPrimitive.ScrollDownButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectScrollDownButton,
|
||||||
|
SelectScrollUpButton,
|
||||||
|
SelectSeparator,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react"
|
||||||
|
|
||||||
|
export const useImageLoader = (src: string) => {
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false)
|
||||||
|
const [error, setError] = useState<Error | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const img = new Image()
|
||||||
|
img.src = src
|
||||||
|
|
||||||
|
img.onload = () => {
|
||||||
|
setIsLoaded(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
img.onerror = (e) => {
|
||||||
|
setError(e as unknown as Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
img.onload = null
|
||||||
|
img.onerror = null
|
||||||
|
}
|
||||||
|
}, [src])
|
||||||
|
|
||||||
|
return { isLoaded, error }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.tsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import HeroSection from "../components/HeroSection"
|
||||||
|
import IntroSection from "../components/IntroSection"
|
||||||
|
import SearchSection from "../components/SearchSection"
|
||||||
|
|
||||||
|
const HomePage = () => {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<HeroSection />
|
||||||
|
<IntroSection />
|
||||||
|
<SearchSection />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HomePage
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
|
import { getMigrantById } from "../services/migrantService";
|
||||||
|
import type { MigrantProfile } from "../types/migrant";
|
||||||
|
import MigrantProfileComponent from "../components/MigrantProfileComponent";
|
||||||
|
import LoadingSpinner from "../components/LoadingSpinner";
|
||||||
|
|
||||||
|
const MigrantProfilePage = () => {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [migrant, setMigrant] = useState<MigrantProfile | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchMigrant = async () => {
|
||||||
|
try {
|
||||||
|
if (id) {
|
||||||
|
const data = await getMigrantById(id);
|
||||||
|
if (data) {
|
||||||
|
setMigrant(data);
|
||||||
|
} else {
|
||||||
|
// Migrant not found, redirect to 404
|
||||||
|
navigate("/not-found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching migrant:", error);
|
||||||
|
navigate("/not-found");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchMigrant();
|
||||||
|
}, [id, navigate]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!migrant) {
|
||||||
|
return null; // This should not happen as we redirect to 404 if migrant is null
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MigrantProfileComponent migrant={migrant} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MigrantProfilePage;
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
const NotFoundPage = () => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-screen px-4 text-center">
|
||||||
|
<h1 className="text-6xl font-bold mb-4">404</h1>
|
||||||
|
<h2 className="text-2xl font-semibold mb-6">Record Not Found</h2>
|
||||||
|
<p className="text-gray-600 mb-8 max-w-md">
|
||||||
|
We couldn't find the migrant record you're looking for. It may have been removed or never existed.
|
||||||
|
</p>
|
||||||
|
<Link to="/" className="btn btn-primary">
|
||||||
|
Return to Homepage
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default NotFoundPage
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
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,33 @@
|
||||||
|
export interface Photo {
|
||||||
|
url: string
|
||||||
|
caption?: string
|
||||||
|
year?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelatedMigrant {
|
||||||
|
id: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
relationship: string
|
||||||
|
photoUrl?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MigrantProfile {
|
||||||
|
id: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
middleName?: string
|
||||||
|
birthDate?: string
|
||||||
|
birthPlace?: string
|
||||||
|
ageAtMigration: number
|
||||||
|
yearOfArrival: number
|
||||||
|
regionOfOrigin: string
|
||||||
|
settlementLocation: string
|
||||||
|
occupation?: string
|
||||||
|
deathDate?: string
|
||||||
|
deathPlace?: string
|
||||||
|
mainPhoto?: string
|
||||||
|
biography?: string
|
||||||
|
photos?: Photo[]
|
||||||
|
relatedMigrants?: RelatedMigrant[]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
export interface SearchParams {
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
ageAtMigration: string | number
|
||||||
|
yearOfArrival: string | number
|
||||||
|
regionOfOrigin: string
|
||||||
|
settlementLocation: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
id: string
|
||||||
|
firstName: string
|
||||||
|
lastName: string
|
||||||
|
ageAtMigration: number
|
||||||
|
yearOfArrival: number
|
||||||
|
regionOfOrigin: string
|
||||||
|
settlementLocation: string
|
||||||
|
photoUrl?: string
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
export const formatDate = (dateString: string | undefined): string => {
|
||||||
|
if (!dateString) return "Unknown"
|
||||||
|
|
||||||
|
// This is a simple formatter, but you could use libraries like date-fns for more complex formatting
|
||||||
|
return dateString
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatYear = (year: number | undefined): string => {
|
||||||
|
if (!year) return "Unknown"
|
||||||
|
return year.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const formatName = (firstName: string, middleName?: string, lastName?: string): string => {
|
||||||
|
let fullName = firstName
|
||||||
|
if (middleName) fullName += ` ${middleName}`
|
||||||
|
if (lastName) fullName += ` ${lastName}`
|
||||||
|
return fullName
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import path from "path";
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@": path.resolve(__dirname, "./src"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue