implement API integration for fetching and posting

This commit is contained in:
mark 2025-05-19 18:10:30 +08:00
parent 37a6be3504
commit 75e2e9c964
52 changed files with 2713 additions and 1241 deletions

209
package-lock.json generated
View File

@ -9,10 +9,14 @@
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@radix-ui/react-avatar": "^1.1.9", "@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-label": "^2.1.6", "@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-select": "^2.2.4", "@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-tabs": "^1.1.11",
"@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", "axios": "^1.9.0",
@ -20,9 +24,11 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.11.0", "framer-motion": "^12.11.0",
"lucide-react": "^0.510.0", "lucide-react": "^0.510.0",
"next-themes": "^0.4.6",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0" "tailwind-merge": "^3.3.0"
}, },
"devDependencies": { "devDependencies": {
@ -1097,6 +1103,36 @@
} }
} }
}, },
"node_modules/@radix-ui/react-checkbox": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.1.tgz",
"integrity": "sha512-xTaLKAO+XXMPK/BpVTSaAAhlefmvMSACjIhK9mGsImvX2ljcTDm8VGR1CuS1uYcNdR5J+oiOhoJZc5un6bh3VQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.2",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-collection": { "node_modules/@radix-ui/react-collection": {
"version": "1.1.6", "version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz",
@ -1225,6 +1261,35 @@
} }
} }
}, },
"node_modules/@radix-ui/react-dropdown-menu": {
"version": "2.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.14.tgz",
"integrity": "sha512-lzuyNjoWOoaMFE/VC5FnAAYM16JmQA8ZmucOXtlhm2kKR5TSU95YLAueQ4JYuRmUJmBvSqXaVFGIfuukybwZJQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-menu": "2.1.14",
"@radix-ui/react-primitive": "2.1.2",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-focus-guards": { "node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz",
@ -1302,6 +1367,46 @@
} }
} }
}, },
"node_modules/@radix-ui/react-menu": {
"version": "2.1.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.14.tgz",
"integrity": "sha512-0zSiBAIFq9GSKoSH5PdEaQeRB3RnEGxC+H2P0egtnKoKKLNBH8VBHyVO6/jskhjAezhOIplyRUj7U2lds9A+Yg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.6",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.9",
"@radix-ui/react-focus-guards": "1.1.2",
"@radix-ui/react-focus-scope": "1.1.6",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-popper": "1.2.6",
"@radix-ui/react-portal": "1.1.8",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.2",
"@radix-ui/react-roving-focus": "1.1.9",
"@radix-ui/react-slot": "1.2.2",
"@radix-ui/react-use-callback-ref": "1.1.1",
"aria-hidden": "^1.2.4",
"react-remove-scroll": "^2.6.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": { "node_modules/@radix-ui/react-popper": {
"version": "1.2.6", "version": "1.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.6.tgz",
@ -1401,6 +1506,37 @@
} }
} }
}, },
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.9.tgz",
"integrity": "sha512-ZzrIFnMYHHCNqSNCsuN6l7wlewBEq0O0BCSBkabJMFXVO51LRUTq71gLP1UxFvmrXElqmPjA5VX7IqC9VpazAQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-collection": "1.1.6",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.2",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-select": { "node_modules/@radix-ui/react-select": {
"version": "2.2.4", "version": "2.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.4.tgz",
@ -1443,6 +1579,29 @@
} }
} }
}, },
"node_modules/@radix-ui/react-separator": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.6.tgz",
"integrity": "sha512-Izof3lPpbCfTM7WDta+LRkz31jem890VjEvpVRoWQNKpDUMMVffuyq854XPGP1KYGWWmjmYvHvPFeocWhFCy1w==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": { "node_modules/@radix-ui/react-slot": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.2.tgz",
@ -1460,6 +1619,36 @@
} }
} }
}, },
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.11.tgz",
"integrity": "sha512-4FiKSVoXqPP/KfzlB7lwwqoFV6EPwkrrqGp9cUYXjwDYHhvpnqq79P+EPHKcdoTE7Rl8w/+6s9rTlsfXHES9GA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.4",
"@radix-ui/react-primitive": "2.1.2",
"@radix-ui/react-roving-focus": "1.1.9",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toast": { "node_modules/@radix-ui/react-toast": {
"version": "1.2.13", "version": "1.2.13",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.13.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.13.tgz",
@ -4420,6 +4609,16 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.19", "version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
@ -5148,6 +5347,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/sonner": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.3.tgz",
"integrity": "sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",

View File

@ -11,10 +11,14 @@
}, },
"dependencies": { "dependencies": {
"@radix-ui/react-avatar": "^1.1.9", "@radix-ui/react-avatar": "^1.1.9",
"@radix-ui/react-checkbox": "^1.3.1",
"@radix-ui/react-dialog": "^1.1.13", "@radix-ui/react-dialog": "^1.1.13",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-label": "^2.1.6", "@radix-ui/react-label": "^2.1.6",
"@radix-ui/react-select": "^2.2.4", "@radix-ui/react-select": "^2.2.4",
"@radix-ui/react-separator": "^1.1.6",
"@radix-ui/react-slot": "^1.2.2", "@radix-ui/react-slot": "^1.2.2",
"@radix-ui/react-tabs": "^1.1.11",
"@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", "axios": "^1.9.0",
@ -22,9 +26,11 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"framer-motion": "^12.11.0", "framer-motion": "^12.11.0",
"lucide-react": "^0.510.0", "lucide-react": "^0.510.0",
"next-themes": "^0.4.6",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-router-dom": "^7.6.0", "react-router-dom": "^7.6.0",
"sonner": "^2.0.3",
"tailwind-merge": "^3.3.0" "tailwind-merge": "^3.3.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -2,11 +2,13 @@ import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
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";
import LoginPage from "./components/LoginPage"; import LoginPage from "./components/admin/LoginPage";
import Migrants from "./components/Migrants"; import Migrants from "./components/admin/Migrants";
import ProfileSettings from "./components/ui/ProfileSettings"; import ProfileSettings from "./components/ui/ProfileSettings";
import AdminDashboardPage from "./pages/AdminDashboardPage"; import AdminDashboardPage from "./components/admin/AdminDashboard";
import HomePage from "./pages/HomePage"; import HomePage from "./pages/HomePage";
import RegisterPage from "./components/admin/Register";
import AddMigrantPage from "./components/admin/AddMigrant";
function App() { function App() {
return ( return (
@ -15,7 +17,9 @@ function App() {
<Route path="/admin/settings/profile" element={<ProfileSettings />} /> <Route path="/admin/settings/profile" element={<ProfileSettings />} />
<Route path="/admin/migrants" element={<Migrants />} /> <Route path="/admin/migrants" element={<Migrants />} />
<Route path="/admin" element={<AdminDashboardPage />} /> <Route path="/admin" element={<AdminDashboardPage />} />
<Route path="/admin/migrants/add" element={<AddMigrantPage />} />
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/migrants/:id" element={<MigrantProfilePage />} /> <Route path="/migrants/:id" element={<MigrantProfilePage />} />
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />

View File

@ -1,360 +0,0 @@
"use client"
import { useEffect, useState } from "react"
import { useNavigate } from "react-router-dom"
import { motion } from "framer-motion"
import {
Users,
Database,
FileText,
Settings,
BarChart2,
Calendar,
Clock,
PlusCircle,
Search,
User,
Bell,
Home,
} from "lucide-react"
import { Link } from "react-router-dom"
export default function AdminDashboard() {
const [isFirstLoad, setIsFirstLoad] = useState(true)
const [isProfileDropdownOpen, setIsProfileDropdownOpen] = useState(false)
const navigate = useNavigate()
useEffect(() => {
// Check if user is authenticated
const token = localStorage.getItem("adminToken")
if (!token) {
navigate("/admin")
return
}
// Check if we're navigating from another admin page
const hasVisitedAdmin = localStorage.getItem("adminNavigation")
// Skip loading if we're navigating from another admin page
if (hasVisitedAdmin) {
setIsFirstLoad(false)
} else {
// Set flag to indicate we've visited an admin page
localStorage.setItem("adminNavigation", "true")
// Only show loading state on first load
if (isFirstLoad) {
// Simulate loading dashboard data
const timer = setTimeout(() => {
setIsFirstLoad(false)
// Show welcome toast
}, 500) // Reduced loading time for better UX
return () => clearTimeout(timer)
}
}
}, [isFirstLoad, navigate])
const handleLogout = () => {
localStorage.removeItem("adminToken")
localStorage.removeItem("adminNavigation") // Clear navigation flag on logout
navigate("/login")
}
// If it's the first load, show a loading state
if (isFirstLoad) {
return (
<div className="min-h-screen flex items-center justify-center bg-[#E8DCCA]/10">
<div className="text-center">
<div className="w-16 h-16 border-4 border-[#9B2335] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="mt-4 text-gray-600">Loading dashboard...</p>
</div>
</div>
)
}
return (
<div className="min-h-screen flex bg-[#E8DCCA]/10">
{/* Sidebar */}
<div className="w-64 bg-[#1A2A57] text-white">
<div className="p-4 border-b border-[#1A2A57]/30">
<h2 className="text-xl font-serif font-bold">Italian Migrants</h2>
<p className="text-sm text-white/70">Northern Territory DB</p>
</div>
<nav className="mt-6 px-4">
<div className="space-y-1">
<Link to="/admin" className="flex items-center px-4 py-3 text-white bg-[#1A2A57]/40 rounded-md">
<Home className="h-5 w-5 mr-3" />
Dashboard
</Link>
<Link
to="/admin/migrants"
className="flex items-center px-4 py-3 text-white/80 hover:bg-[#1A2A57]/40 rounded-md"
>
<User className="h-5 w-5 mr-3" />
Migrants
</Link>
<Link to="#" className="flex items-center px-4 py-3 text-white/80 hover:bg-[#1A2A57]/40 rounded-md">
<FileText className="h-5 w-5 mr-3" />
Reports
</Link>
<Link to="#" className="flex items-center px-4 py-3 text-white/80 hover:bg-[#1A2A57]/40 rounded-md">
<Database className="h-5 w-5 mr-3" />
Database
</Link>
<Link
to="/admin/settings/profile"
className="flex items-center px-4 py-3 text-white/80 hover:bg-[#1A2A57]/40 rounded-md"
>
<Settings className="h-5 w-5 mr-3" />
Settings
</Link>
</div>
</nav>
<div className="absolute bottom-0 w-64 p-4 border-t border-[#1A2A57]/30">
<div className="flex items-center">
<div className="h-8 w-8 rounded-full bg-white text-[#1A2A57] flex items-center justify-center">
<User className="h-5 w-5" />
</div>
<span className="ml-2 text-sm">Admin User</span>
</div>
</div>
</div>
{/* Main content */}
<div className="flex-1 overflow-auto">
{/* Header */}
<div className="bg-white shadow-sm border-b border-gray-200">
<div className="flex justify-between items-center px-6 py-4">
<h1 className="text-xl font-medium text-[#1A2A57]">Admin Portal</h1>
<div className="flex items-center space-x-4">
<div className="relative">
<input
type="text"
placeholder="Search records..."
className="pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#01796F] focus:border-[#01796F]"
/>
<Search className="absolute left-3 top-2.5 h-5 w-5 text-gray-400" />
</div>
{/* Notifications */}
<button
className="relative p-2 text-gray-500 hover:text-gray-700 focus:outline-none"
>
<Bell className="h-5 w-5" />
<span className="absolute top-0 right-0 h-2 w-2 rounded-full bg-[#9B2335]"></span>
</button>
{/* Profile dropdown */}
<div className="relative">
<button
className="h-8 w-8 rounded-full bg-[#9B2335] text-white flex items-center justify-center focus:outline-none"
onClick={() => setIsProfileDropdownOpen(!isProfileDropdownOpen)}
>
<User className="h-4 w-4" />
</button>
{isProfileDropdownOpen && (
<motion.div
className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-10"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2 }}
>
<Link
to="/admin/settings/profile"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={() => setIsProfileDropdownOpen(false)}
>
Your Profile
</Link>
<Link
to="#"
className="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
Settings
</Link>
<button
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
onClick={handleLogout}
>
Sign out
</button>
</motion.div>
)}
</div>
</div>
</div>
</div>
{/* Dashboard content */}
<div className="p-6">
{/* Stats cards */}
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div className="bg-white rounded-lg shadow p-6 border-t-4 border-[#9B2335]">
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 text-sm">Total Migrants</p>
<h3 className="text-3xl font-bold mt-1">256</h3>
</div>
<div className="p-2 bg-[#9B2335]/10 rounded-md">
<Users className="h-6 w-6 text-[#9B2335]" />
</div>
</div>
<div className="mt-4 text-sm text-green-600">+12 this month</div>
</div>
<div className="bg-white rounded-lg shadow p-6 border-t-4 border-[#01796F]">
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 text-sm">Total Records</p>
<h3 className="text-3xl font-bold mt-1">1,248</h3>
</div>
<div className="p-2 bg-[#01796F]/10 rounded-md">
<Database className="h-6 w-6 text-[#01796F]" />
</div>
</div>
<div className="mt-4 text-sm text-green-600">+86 this month</div>
</div>
<div className="bg-white rounded-lg shadow p-6 border-t-4 border-[#FFC857]">
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 text-sm">Recent Updates</p>
<h3 className="text-3xl font-bold mt-1">24</h3>
</div>
<div className="p-2 bg-[#FFC857]/10 rounded-md">
<Clock className="h-6 w-6 text-[#FFC857]" />
</div>
</div>
<div className="mt-4 text-sm text-blue-600">Last update: Today</div>
</div>
<div className="bg-white rounded-lg shadow p-6 border-t-4 border-[#1A2A57]">
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 text-sm">Pending Tasks</p>
<h3 className="text-3xl font-bold mt-1">8</h3>
</div>
<div className="p-2 bg-[#1A2A57]/10 rounded-md">
<Calendar className="h-6 w-6 text-[#1A2A57]" />
</div>
</div>
<div className="mt-4 text-sm text-orange-500">3 require attention</div>
</div>
</motion.div>
{/* Quick actions */}
<motion.div
className="bg-white rounded-lg shadow mb-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-medium">Quick Actions</h2>
</div>
<div className="p-6 grid grid-cols-1 md:grid-cols-3 gap-4">
<Link
to="/admin/migrants"
className="flex items-center justify-center px-4 py-3 bg-[#9B2335] text-white rounded-md hover:bg-[#9B2335]/90"
>
<PlusCircle className="h-5 w-5 mr-2" />
Add New Migrant
</Link>
<button
className="flex items-center justify-center px-4 py-3 bg-[#01796F] text-white rounded-md hover:bg-[#01796F]/90"
>
<FileText className="h-5 w-5 mr-2" />
Generate Report
</button>
<button
className="flex items-center justify-center px-4 py-3 bg-[#1A2A57] text-white rounded-md hover:bg-[#1A2A57]/90"
>
<Database className="h-5 w-5 mr-2" />
Import Records
</button>
</div>
</motion.div>
{/* Recent activity */}
<motion.div
className="bg-white rounded-lg shadow"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.4 }}
>
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-medium">Recent Activity</h2>
</div>
<div className="p-6">
<div className="space-y-6">
{[
{ action: "Added new migrant", user: "Admin", time: "10 minutes ago", type: "add" },
{ action: "Updated record #1248", user: "Admin", time: "2 hours ago", type: "update" },
{ action: "Generated monthly report", user: "System", time: "Yesterday", type: "report" },
{ action: "Deleted duplicate record", user: "Admin", time: "2 days ago", type: "delete" },
{ action: "Imported 15 new records", user: "Admin", time: "1 week ago", type: "import" },
].map((activity, index) => (
<motion.div
key={index}
className="flex items-start"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: 0.5 + index * 0.1 }}
>
<div
className={`p-2 rounded-full mr-4 ${
activity.type === "add"
? "bg-green-100 text-green-600"
: activity.type === "update"
? "bg-blue-100 text-blue-600"
: activity.type === "delete"
? "bg-red-100 text-red-600"
: activity.type === "report"
? "bg-purple-100 text-purple-600"
: "bg-yellow-100 text-yellow-600"
}`}
>
{activity.type === "add" ? (
<PlusCircle className="h-5 w-5" />
) : activity.type === "update" ? (
<FileText className="h-5 w-5" />
) : activity.type === "delete" ? (
<Users className="h-5 w-5" />
) : activity.type === "report" ? (
<BarChart2 className="h-5 w-5" />
) : (
<Database className="h-5 w-5" />
)}
</div>
<div>
<p className="font-medium">{activity.action}</p>
<p className="text-sm text-gray-500">
By {activity.user} {activity.time}
</p>
</div>
</motion.div>
))}
</div>
</div>
</motion.div>
</div>
</div>
</div>
)
}

View File

@ -1,21 +0,0 @@
import Sidebar from "./Sidebar"
import Header from "./Header"
import StatsCards from "./StatsCards"
import QuickActions from "./QuickActions"
import RecentActivity from "./RecentActivity"
export default function DashboardLayout() {
return (
<div className="min-h-screen flex bg-[#E8DCCA]/10">
<Sidebar />
<div className="flex-1 overflow-auto">
<Header />
<div className="p-6">
<StatsCards />
<QuickActions />
<RecentActivity />
</div>
</div>
</div>
)
}

View File

@ -1,25 +0,0 @@
import { Search, Bell, ChevronDown } from "lucide-react"
export default function Header() {
return (
<div className="flex items-center justify-between px-6 py-4 border-b border-[#1A2A57]/20 bg-white shadow-sm">
<div className="relative w-80">
<input
type="text"
placeholder="Search..."
className="w-full px-4 py-2 rounded-md bg-gray-100 focus:outline-none focus:ring-2 focus:ring-[#9B2335]"
/>
<Search className="absolute top-2.5 right-3 h-5 w-5 text-gray-500" />
</div>
<div className="flex items-center space-x-6">
<Bell className="h-6 w-6 text-[#1A2A57] cursor-pointer" />
<div className="flex items-center space-x-2 cursor-pointer">
<div className="h-8 w-8 rounded-full bg-[#1A2A57] text-white flex items-center justify-center">
A
</div>
<ChevronDown className="h-4 w-4 text-[#1A2A57]" />
</div>
</div>
</div>
)
}

View File

@ -1,53 +0,0 @@
import { Link } from "react-router-dom"
import { Home, User, FileText, Database, Settings } from "lucide-react"
export default function Sidebar() {
return (
<div className="w-64 bg-[#1A2A57] text-white">
<div className="p-4 border-b border-[#1A2A57]/30">
<h2 className="text-xl font-serif font-bold">Italian Migrants</h2>
<p className="text-sm text-white/70">Northern Territory DB</p>
</div>
<nav className="mt-6 px-4">
<div className="space-y-1">
<Link to="/admin" className="flex items-center px-4 py-3 text-white bg-[#1A2A57]/40 rounded-md">
<Home className="h-5 w-5 mr-3" />
Dashboard
</Link>
<Link
to="/admin/migrants"
className="flex items-center px-4 py-3 text-white/80 hover:bg-[#1A2A57]/40 rounded-md"
>
<User className="h-5 w-5 mr-3" />
Migrants
</Link>
<Link to="#" className="flex items-center px-4 py-3 text-white/80 hover:bg-[#1A2A57]/40 rounded-md">
<FileText className="h-5 w-5 mr-3" />
Reports
</Link>
<Link to="#" className="flex items-center px-4 py-3 text-white/80 hover:bg-[#1A2A57]/40 rounded-md">
<Database className="h-5 w-5 mr-3" />
Database
</Link>
<Link
to="/admin/settings/profile"
className="flex items-center px-4 py-3 text-white/80 hover:bg-[#1A2A57]/40 rounded-md"
>
<Settings className="h-5 w-5 mr-3" />
Settings
</Link>
</div>
</nav>
<div className="absolute bottom-0 w-64 p-4 border-t border-[#1A2A57]/30">
<div className="flex items-center">
<div className="h-8 w-8 rounded-full bg-white text-[#1A2A57] flex items-center justify-center">
<User className="h-5 w-5" />
</div>
<span className="ml-2 text-sm">Admin User</span>
</div>
</div>
</div>
)
}

View File

@ -1,65 +0,0 @@
import { motion } from "framer-motion"
import { Users, Database, Clock, Calendar } from "lucide-react"
export default function StatsCards() {
return (
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
<div className="bg-white rounded-lg shadow p-6 border-t-4 border-[#9B2335]">
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 text-sm">Total Migrants</p>
<h3 className="text-3xl font-bold mt-1">256</h3>
</div>
<div className="p-2 bg-[#9B2335]/10 rounded-md">
<Users className="h-6 w-6 text-[#9B2335]" />
</div>
</div>
<div className="mt-4 text-sm text-green-600">+12 this month</div>
</div>
<div className="bg-white rounded-lg shadow p-6 border-t-4 border-[#01796F]">
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 text-sm">Total Records</p>
<h3 className="text-3xl font-bold mt-1">1,248</h3>
</div>
<div className="p-2 bg-[#01796F]/10 rounded-md">
<Database className="h-6 w-6 text-[#01796F]" />
</div>
</div>
<div className="mt-4 text-sm text-green-600">+86 this month</div>
</div>
<div className="bg-white rounded-lg shadow p-6 border-t-4 border-[#FFC857]">
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 text-sm">Recent Updates</p>
<h3 className="text-3xl font-bold mt-1">24</h3>
</div>
<div className="p-2 bg-[#FFC857]/10 rounded-md">
<Clock className="h-6 w-6 text-[#FFC857]" />
</div>
</div>
<div className="mt-4 text-sm text-blue-600">Last update: Today</div>
</div>
<div className="bg-white rounded-lg shadow p-6 border-t-4 border-[#1A2A57]">
<div className="flex justify-between items-start">
<div>
<p className="text-gray-500 text-sm">Pending Tasks</p>
<h3 className="text-3xl font-bold mt-1">8</h3>
</div>
<div className="p-2 bg-[#1A2A57]/10 rounded-md">
<Calendar className="h-6 w-6 text-[#1A2A57]" />
</div>
</div>
<div className="mt-4 text-sm text-orange-500">3 require attention</div>
</div>
</motion.div>
)
}

View File

@ -1,602 +0,0 @@
"use client"
import { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom"
import { motion } from "framer-motion"
import { Link } from "react-router-dom"
import {
Home,
User,
FileText,
Database,
Settings,
Search,
Filter,
MoreHorizontal,
Edit,
Upload,
Trash2,
Plus,
ChevronUp,
} from "lucide-react"
interface Migrant {
id: number
name: string
birthDate: string
arrivalDate: string
origin: string
status: "Verified" | "Incomplete" | "Pending"
photos: number
}
export default function Migrants() {
const [isFirstLoad, setIsFirstLoad] = useState(true)
const [migrants, setMigrants] = useState<Migrant[]>([])
const [selectedMigrants, setSelectedMigrants] = useState<number[]>([])
const [activeDropdown, setActiveDropdown] = useState<number | null>(null)
const [searchQuery, setSearchQuery] = useState("")
const [arrivalDateMin, setArrivalDateMin] = useState("")
const [arrivalDateMax, setArrivalDateMax] = useState("")
const navigate = useNavigate()
// Add a handleLogout function that clears the adminNavigation flag
const handleLogout = () => {
localStorage.removeItem("adminToken")
localStorage.removeItem("adminNavigation") // Clear navigation flag on logout
navigate("/login")
}
// Check authentication and load data
useEffect(() => {
const token = localStorage.getItem("adminToken")
if (!token) {
navigate("/login")
return
}
// Check if we're navigating from another admin page
const hasVisitedAdmin = localStorage.getItem("adminNavigation")
// Skip loading if we're navigating from another admin page
if (hasVisitedAdmin) {
setIsFirstLoad(false)
// Simulate API call to fetch migrants data without loading state
setMigrants([
{
id: 3,
name: "Antonio Esposito",
birthDate: "2/18/1940",
arrivalDate: "9/5/1962",
origin: "Rome, Italy",
status: "Verified",
photos: 5,
},
{
id: 8,
name: "Giovanna Ferraro",
birthDate: "3/11/1955",
arrivalDate: "8/5/1978",
origin: "Turin, Italy",
status: "Incomplete",
photos: 0,
},
{
id: 5,
name: "Giuseppe Colombo",
birthDate: "4/7/1935",
arrivalDate: "11/28/1955",
origin: "Venice, Italy",
status: "Verified",
photos: 2,
},
{
id: 4,
name: "Lucia Romano",
birthDate: "8/30/1958",
arrivalDate: "1/10/1980",
origin: "Milan, Italy",
status: "Incomplete",
photos: 0,
},
{
id: 1,
name: "Marco Rossi",
birthDate: "5/12/1945",
arrivalDate: "3/15/1968",
origin: "Sicily, Italy",
status: "Verified",
photos: 3,
},
{
id: 6,
name: "Maria Ricci",
birthDate: "12/15/1950",
arrivalDate: "6/20/1972",
origin: "Florence, Italy",
status: "Pending",
photos: 1,
},
{
id: 7,
name: "Paolo Marino",
birthDate: "9/22/1943",
arrivalDate: "4/17/1965",
origin: "Palermo, Italy",
status: "Verified",
photos: 4,
},
])
} else {
// Set flag to indicate we've visited an admin page
localStorage.setItem("adminNavigation", "true")
// Only show loading state on first load
if (isFirstLoad) {
// Simulate API call to fetch migrants data
setTimeout(() => {
setMigrants([
{
id: 3,
name: "Antonio Esposito",
birthDate: "2/18/1940",
arrivalDate: "9/5/1962",
origin: "Rome, Italy",
status: "Verified",
photos: 5,
},
{
id: 8,
name: "Giovanna Ferraro",
birthDate: "3/11/1955",
arrivalDate: "8/5/1978",
origin: "Turin, Italy",
status: "Incomplete",
photos: 0,
},
{
id: 5,
name: "Giuseppe Colombo",
birthDate: "4/7/1935",
arrivalDate: "11/28/1955",
origin: "Venice, Italy",
status: "Verified",
photos: 2,
},
{
id: 4,
name: "Lucia Romano",
birthDate: "8/30/1958",
arrivalDate: "1/10/1980",
origin: "Milan, Italy",
status: "Incomplete",
photos: 0,
},
{
id: 1,
name: "Marco Rossi",
birthDate: "5/12/1945",
arrivalDate: "3/15/1968",
origin: "Sicily, Italy",
status: "Verified",
photos: 3,
},
{
id: 6,
name: "Maria Ricci",
birthDate: "12/15/1950",
arrivalDate: "6/20/1972",
origin: "Florence, Italy",
status: "Pending",
photos: 1,
},
{
id: 7,
name: "Paolo Marino",
birthDate: "9/22/1943",
arrivalDate: "4/17/1965",
origin: "Palermo, Italy",
status: "Verified",
photos: 4,
},
])
setIsFirstLoad(false)
}, 1000)
}
}
}, [isFirstLoad, navigate])
const handleSelectMigrant = (id: number) => {
setSelectedMigrants((prev) => (prev.includes(id) ? prev.filter((migrantId) => migrantId !== id) : [...prev, id]))
}
const handleSelectAll = () => {
if (selectedMigrants.length === migrants.length) {
setSelectedMigrants([])
} else {
setSelectedMigrants(migrants.map((migrant) => migrant.id))
}
}
const handleDropdownToggle = (id: number) => {
setActiveDropdown(activeDropdown === id ? null : id)
}
const filteredMigrants = migrants.filter((migrant) => migrant.name.toLowerCase().includes(searchQuery.toLowerCase()))
// If it's the first load, show a loading state
if (isFirstLoad) {
return (
<div className="min-h-screen flex items-center justify-center bg-[#E8DCCA]/10">
<div className="text-center">
<div className="w-16 h-16 border-4 border-[#9B2335] border-t-transparent rounded-full animate-spin mx-auto"></div>
<p className="mt-4 text-gray-600">Loading migrants data...</p>
</div>
</div>
)
}
return (
<div className="min-h-screen flex">
{/* Sidebar */}
<div className="w-64 bg-[#1A2A57] text-white">
<div className="p-4 border-b border-[#1A2A57]/30">
<h2 className="text-xl font-serif font-bold">Italian Migrants</h2>
<p className="text-sm text-white/70">Northern Territory DB</p>
</div>
<nav className="mt-6 px-4">
<div className="space-y-1">
<Link
to="/admin"
className="flex items-center px-4 py-3 text-white/80 hover:bg-[#1A2A57]/40 rounded-md"
>
<Home className="h-5 w-5 mr-3" />
Dashboard
</Link>
<Link to="/admin/migrants" className="flex items-center px-4 py-3 text-white bg-[#1A2A57]/40 rounded-md">
<User className="h-5 w-5 mr-3" />
Migrants
</Link>
<Link to="#" className="flex items-center px-4 py-3 text-white/80 hover:bg-[#1A2A57]/40 rounded-md">
<FileText className="h-5 w-5 mr-3" />
Reports
</Link>
<Link to="#" className="flex items-center px-4 py-3 text-white/80 hover:bg-[#1A2A57]/40 rounded-md">
<Database className="h-5 w-5 mr-3" />
Database
</Link>
<Link
to="/admin/settings/profile"
className="flex items-center px-4 py-3 text-white/80 hover:bg-[#1A2A57]/40 rounded-md"
>
<Settings className="h-5 w-5 mr-3" />
Settings
</Link>
</div>
</nav>
{/* Add this to the bottom user section in the sidebar */}
<div className="absolute bottom-0 w-64 p-4 border-t border-[#1A2A57]/30">
<button
onClick={handleLogout}
className="flex items-center w-full px-4 py-2 text-white/80 hover:bg-[#1A2A57]/40 rounded-md"
>
<div className="h-8 w-8 rounded-full bg-white text-[#1A2A57] flex items-center justify-center mr-2">
<User className="h-5 w-5" />
</div>
<span className="text-sm">Admin User</span>
</button>
</div>
</div>
{/* Main content */}
<div className="flex-1 overflow-auto">
{/* Header */}
<div className="bg-white shadow-sm border-b border-gray-200">
<div className="flex justify-between items-center px-6 py-4">
<h1 className="text-xl font-medium text-[#1A2A57]">Admin Portal</h1>
<div className="text-sm text-gray-600">Northern Territory</div>
</div>
</div>
{/* Migrants Management content */}
<div className="p-6">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3 }}>
<div className="flex justify-between items-center mb-6">
<div>
<h2 className="text-2xl font-bold text-gray-800">Migrants Management</h2>
<p className="text-gray-600">Manage and organize migrant records</p>
</div>
<button
className="px-4 py-2 bg-[#01796F] text-white rounded-md hover:bg-[#01796F]/90 flex items-center"
>
<Plus className="h-4 w-4 mr-2" />
Add Migrant
</button>
</div>
{/* Migrants Database */}
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden">
<div className="p-4 border-b border-gray-200">
<h3 className="text-lg font-medium text-[#1A2A57]">Migrants Database</h3>
</div>
{/* Search and filters */}
<div className="p-4 flex flex-wrap items-center justify-between gap-4 border-b border-gray-200">
<div className="flex flex-wrap items-center gap-4 w-full lg:w-auto">
<div className="relative w-full lg:w-64">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" />
<input
type="text"
placeholder="Search migrants..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 pr-4 py-2 w-full border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-[#01796F] focus:border-[#01796F]"
/>
</div>
<div className="flex items-center gap-2 w-full lg:w-auto">
<span className="text-sm text-gray-500 whitespace-nowrap">Arrival Date:</span>
<input
type="date"
placeholder="From"
value={arrivalDateMin}
onChange={(e) => setArrivalDateMin(e.target.value)}
className="border border-gray-300 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-[#01796F] focus:border-[#01796F] w-32"
/>
<span className="text-sm text-gray-500">to</span>
<input
type="date"
placeholder="To"
value={arrivalDateMax}
onChange={(e) => setArrivalDateMax(e.target.value)}
className="border border-gray-300 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-[#01796F] focus:border-[#01796F] w-32"
/>
</div>
</div>
<div className="flex items-center gap-4">
<button className="p-2 border border-gray-300 rounded-md hover:bg-gray-50">
<Filter className="h-4 w-4 text-gray-500" />
</button>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">Show:</span>
<select className="border border-gray-300 rounded-md px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-[#01796F] focus:border-[#01796F]">
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
<div className="relative">
<button className="px-3 py-1 border border-gray-300 rounded-md text-sm hover:bg-gray-50 flex items-center gap-1">
Batch Actions
<svg
className="h-4 w-4 text-gray-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
</div>
</div>
</div>
{/* Table */}
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="px-6 py-3 text-left">
<div className="flex items-center">
<input
type="checkbox"
className="h-4 w-4 text-[#01796F] focus:ring-[#01796F] border-gray-300 rounded"
checked={selectedMigrants.length === migrants.length && migrants.length > 0}
onChange={handleSelectAll}
/>
</div>
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
ID
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
<div className="flex items-center">
Name
<ChevronUp className="h-4 w-4 ml-1" />
</div>
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Birth Date
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Arrival Date
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Origin
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Status
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Photos
</th>
<th
scope="col"
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
>
Actions
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredMigrants.map((migrant) => (
<tr key={migrant.id} className="hover:bg-gray-50">
<td className="px-6 py-4 whitespace-nowrap">
<input
type="checkbox"
className="h-4 w-4 text-[#01796F] focus:ring-[#01796F] border-gray-300 rounded"
checked={selectedMigrants.includes(migrant.id)}
onChange={() => handleSelectMigrant(migrant.id)}
/>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{migrant.id}</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<div className="h-8 w-8 rounded-full bg-gray-200 flex-shrink-0"></div>
<div className="ml-4">
<div className="text-sm font-medium text-gray-900">{migrant.name}</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{migrant.birthDate}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{migrant.arrivalDate}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{migrant.origin}</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
migrant.status === "Verified"
? "bg-green-100 text-green-800"
: migrant.status === "Incomplete"
? "bg-red-100 text-red-800"
: "bg-yellow-100 text-yellow-800"
}`}
>
{migrant.status}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{migrant.photos}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium relative">
<button
onClick={() => handleDropdownToggle(migrant.id)}
className="text-gray-400 hover:text-gray-500"
>
<MoreHorizontal className="h-5 w-5" />
</button>
{activeDropdown === migrant.id && (
<div className="absolute right-0 mt-2 w-48 bg-white rounded-md shadow-lg py-1 z-10 border border-gray-200">
<button
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
<Edit className="h-4 w-4 inline-block mr-2" />
Edit
</button>
<button
className="block w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100"
>
<Upload className="h-4 w-4 inline-block mr-2" />
Upload Photos
</button>
<button
className="block w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-gray-100"
>
<Trash2 className="h-4 w-4 inline-block mr-2" />
Delete
</button>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="px-6 py-3 flex items-center justify-between border-t border-gray-200">
<div className="flex-1 flex justify-between sm:hidden">
<button className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Previous
</button>
<button className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
Next
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing <span className="font-medium">1</span> to{" "}
<span className="font-medium">{filteredMigrants.length}</span> of{" "}
<span className="font-medium">{filteredMigrants.length}</span> results
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<button className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span className="sr-only">Previous</span>
<svg
className="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</button>
<button
aria-current="page"
className="z-10 bg-[#01796F] border-[#01796F] text-white relative inline-flex items-center px-4 py-2 border text-sm font-medium"
>
1
</button>
<button className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50">
<span className="sr-only">Next</span>
<svg
className="h-5 w-5"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
</button>
</nav>
</div>
</div>
</div>
</div>
</motion.div>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,126 @@
"use client"
import { useState } from "react";
import { ArrowLeft, Save } from "lucide-react";
import { Link } from "react-router-dom";
import apiService from "@/services/apiService";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import Header from "../layout/Header";
import Sidebar from "../layout/Sidebar";
import { PersonalInfoTab } from "./migrant/PersonalInfoTab";
import { MigrationDetailsTab } from "./migrant/MigrationDetailsTab";
import { LocationsTab } from "./migrant/LocationsTab";
import { InterneeDetailsTab } from "./migrant/InterneeDetailsTab";
import { PhotosTab } from "./migrant/PhotosTab";
import { NotesTab } from "./migrant/NotesTab";
export default function AddUserPage() {
const [formData, setFormData] = useState({
surname: "",
christian_name: "",
full_name: "",
date_of_birth: "",
date_of_death: "",
place_of_birth: "",
home_at_death: "",
occupation: "",
names_of_parents: "",
names_of_children: "",
data_source: "",
reference: "",
cav: "",
id_card_no: "",
date_of_arrival_australia: "",
date_of_arrival_nt: "",
date_of_naturalisation: "",
corps_issued: "",
no_of_cert: "",
issued_at: "",
});
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { id, value } = e.target;
setFormData(prev => ({ ...prev, [id]: value }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
const res = await apiService.createPerson(formData);
alert("Migrant created successfully!");
console.log(res);
} catch (err) {
alert("Failed to create migrant.");
console.error(err);
}
};
return (
<div className="flex min-h-dvh bg-neutral-50">
<Sidebar />
<div className="flex-1">
<Header title="Add New Migrant" />
<main className="p-6">
<div className="flex items-center mb-6">
<Link to="/admin/migrants">
<Button variant="ghost" size="sm" className="gap-1">
<ArrowLeft className="size-4" /> Back to Migrants
</Button>
</Link>
<h1 className="text-3xl font-serif font-bold text-neutral-800 ml-4">
Add New Migrant
</h1>
</div>
<form onSubmit={handleSubmit}>
<Tabs defaultValue="personal" className="mb-8">
<TabsList className="bg-neutral-100 mb-6">
<TabsTrigger value="personal">Personal Information</TabsTrigger>
<TabsTrigger value="migration">Migration Details</TabsTrigger>
<TabsTrigger value="locations">Locations</TabsTrigger>
<TabsTrigger value="internee">Internee Details</TabsTrigger>
<TabsTrigger value="photos">Photos & Documents</TabsTrigger>
<TabsTrigger value="notes">Additional Notes</TabsTrigger>
</TabsList>
<TabsContent value="personal" className="m-0">
<PersonalInfoTab formData={formData} handleInputChange={handleInputChange} />
</TabsContent>
<TabsContent value="migration" className="m-0">
<MigrationDetailsTab formData={formData} handleInputChange={handleInputChange} />
</TabsContent>
<TabsContent value="locations" className="m-0">
<LocationsTab />
</TabsContent>
<TabsContent value="internee" className="m-0">
<InterneeDetailsTab />
</TabsContent>
<TabsContent value="photos" className="m-0">
<PhotosTab />
</TabsContent>
<TabsContent value="notes" className="m-0">
<NotesTab />
</TabsContent>
</Tabs>
<div className="flex justify-between items-center">
<Button variant="outline" type="button">
Save as Draft
</Button>
<Button type="submit" className="bg-green-700 hover:bg-green-800">
<Save className="mr-2 size-4" /> Save Migrant Record
</Button>
</div>
</form>
</main>
</div>
</div>
);
}

View File

@ -0,0 +1,172 @@
import { useState } from "react";
import {
BarChart3,
Calendar,
Clock,
Database,
FileText,
PlusCircle,
Search,
User,
Users,
} from "lucide-react";
import { Link } from "react-router-dom";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
import Header from "../layout/Header";
import Sidebar from "../layout/Sidebar";
import RecentActivityList from "../common/RecentActivity";
import StatCard from "../common/StatCard";
export default function DashboardPage() {
const [searchQuery, setSearchQuery] = useState("");
return (
<div className="flex min-h-dvh bg-neutral-50">
<Sidebar />
<div className="flex-1">
<Header title="Dashboard" />
<main className="p-6">
<div className="mb-8">
<h1 className="text-3xl font-serif font-bold text-neutral-800 mb-2">
Welcome, Admin
</h1>
<p className="text-neutral-600">
Here's an overview of your Italian Migrants Database
</p>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4 mb-8">
<StatCard
title="Total Migrants"
value="1,248"
description="+12 this month"
icon={<Users className="size-5 text-green-600" />}
/>
<StatCard
title="Recent Additions"
value="24"
description="Last 30 days"
icon={<PlusCircle className="size-5 text-blue-600" />}
/>
<StatCard
title="Pending Reviews"
value="8"
description="Needs attention"
icon={<Clock className="size-5 text-amber-600" />}
/>
<StatCard
title="Total Documents"
value="3,542"
description="Photos and records"
icon={<FileText className="size-5 text-red-600" />}
/>
</div>
<div className="grid gap-6 md:grid-cols-2 mb-8">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-xl font-serif">Quick Search</CardTitle>
<CardDescription>Find migrant records quickly</CardDescription>
</CardHeader>
<CardContent>
<div className="relative">
<Search className="absolute left-3 top-2.5 size-5 text-neutral-500" />
<Input
placeholder="Search by name, birthplace, or arrival date..."
className="pl-10 border-neutral-300"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="mt-4 flex flex-wrap gap-2">
<Button variant="outline" size="sm" className="text-sm">
<Calendar className="mr-1 size-4" /> By Date
</Button>
<Button variant="outline" size="sm" className="text-sm">
<User className="mr-1 size-4" /> By Name
</Button>
<Button variant="outline" size="sm" className="text-sm">
<Database className="mr-1 size-4" /> Advanced
</Button>
</div>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-xl font-serif">Migration Trends</CardTitle>
<CardDescription>Yearly migration patterns</CardDescription>
</CardHeader>
<CardContent className="pt-4">
<div className="h-[180px] flex items-end justify-between gap-2">
{[35, 45, 20, 30, 75, 60, 40, 80, 90, 50].map((height, i) => (
<div key={i} className="relative group flex flex-col items-center">
<div className="absolute -top-7 opacity-0 group-hover:opacity-100 transition-opacity bg-neutral-800 text-white px-2 py-1 rounded text-xs">
{1950 + i * 5}: {height} migrants
</div>
<div
className="w-7 bg-gradient-to-t from-green-600 to-green-400 rounded-t"
style={{ height: `${height * 1.8}px` }}
></div>
<span className="text-xs mt-1 text-neutral-500">{1950 + i * 5}</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
<div className="mb-8">
<Tabs defaultValue="recent">
<div className="flex justify-between items-center mb-4">
<TabsList className="bg-neutral-100">
<TabsTrigger value="recent">Recent Activity</TabsTrigger>
<TabsTrigger value="pending">Pending Reviews</TabsTrigger>
</TabsList>
<Link to="/admin/migrants">
<Button variant="outline" size="sm" className="text-sm">
View All Records
</Button>
</Link>
</div>
<TabsContent value="recent" className="m-0">
<Card>
<CardContent className="p-0">
<RecentActivityList />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="pending" className="m-0">
<Card>
<CardContent className="p-6">
<div className="text-center py-8 text-neutral-500">
<BarChart3 className="mx-auto size-12 mb-4 text-neutral-400" />
<h3 className="text-lg font-medium mb-2">No Pending Reviews</h3>
<p>All migrant records have been reviewed.</p>
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
</main>
</div>
</div>
);
}

View File

@ -7,6 +7,7 @@ import { motion } from "framer-motion"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { Eye, EyeOff, Lock, Mail } from "lucide-react" import { Eye, EyeOff, Lock, Mail } from "lucide-react"
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import apiService from "@/services/apiService"
export default function LoginPage() { export default function LoginPage() {
const [email, setEmail] = useState("") const [email, setEmail] = useState("")
@ -23,19 +24,20 @@ export default function LoginPage() {
// Simulate API call for authentication // Simulate API call for authentication
try { try {
// In a real application, this would be an API call to your Laravel backend const response = await apiService.login({
await new Promise((resolve) => setTimeout(resolve, 1500)) email,
password,
// For demo purposes, hardcoded check })
if (email === "admin@example.com" && password === "password") { console.log("Response:", response.data)
// Store token in localStorage or cookies alert("Login successful!")
localStorage.setItem("adminToken", "demo-token-12345")
navigate("/admin") navigate("/admin")
} catch (error: any) {
console.error("Error submitting form:", error)
if (error.response && error.response.data && error.response.data.message) {
setError(error.response.data.message)
} else { } else {
setError("Invalid email or password") setError("Login failed. Please check your input and try again.")
} }
} catch (err) {
setError("Authentication failed. Please try again.")
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }

View File

@ -0,0 +1,245 @@
"use client"
import { useState } from "react"
import { ArrowUpDown, Download, Filter, MoreHorizontal, PlusCircle, Search, Trash2, Upload } from "lucide-react"
import {Link} from "react-router-dom"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
import { Input } from "@/components/ui/input"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import Header from "@/components/layout/Header"
import Sidebar from "@/components/layout/Sidebar"
// Sample data for migrants
const migrants = [
{
id: 1,
name: "Marco Rossi",
birthDate: "1935-05-12",
birthPlace: "Rome, Italy",
arrivalDate: "1952-08-23",
occupation: "Carpenter",
hasPhotos: true,
},
{
id: 2,
name: "Sofia Bianchi",
birthDate: "1942-11-03",
birthPlace: "Naples, Italy",
arrivalDate: "1960-02-15",
occupation: "Seamstress",
hasPhotos: true,
},
{
id: 3,
name: "Antonio Esposito",
birthDate: "1928-07-22",
birthPlace: "Milan, Italy",
arrivalDate: "1950-10-05",
occupation: "Farmer",
hasPhotos: false,
},
{
id: 4,
name: "Lucia Romano",
birthDate: "1940-03-18",
birthPlace: "Florence, Italy",
arrivalDate: "1958-06-30",
occupation: "Teacher",
hasPhotos: true,
},
{
id: 5,
name: "Giuseppe Colombo",
birthDate: "1932-09-08",
birthPlace: "Venice, Italy",
arrivalDate: "1955-12-10",
occupation: "Fisherman",
hasPhotos: false,
},
]
export default function MigrantsPage() {
const [searchQuery, setSearchQuery] = useState("")
const [selectedMigrants, setSelectedMigrants] = useState<number[]>([])
const toggleSelectAll = () => {
if (selectedMigrants.length === migrants.length) {
setSelectedMigrants([])
} else {
setSelectedMigrants(migrants.map((m) => m.id))
}
}
const toggleSelectMigrant = (id: number) => {
if (selectedMigrants.includes(id)) {
setSelectedMigrants(selectedMigrants.filter((m) => m !== id))
} else {
setSelectedMigrants([...selectedMigrants, id])
}
}
const filteredMigrants = migrants.filter(
(migrant) =>
migrant.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
migrant.birthPlace.toLowerCase().includes(searchQuery.toLowerCase()) ||
migrant.occupation.toLowerCase().includes(searchQuery.toLowerCase()),
)
return (
<div className="flex flex-col md:flex-row min-h-dvh bg-neutral-50">
<Sidebar />
<div className="flex-1 w-full">
<Header title="Migrants Management" />
<main className="p-4 md:p-6">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4 sm:gap-0">
<h1 className="text-2xl md:text-3xl font-serif font-bold text-neutral-800">Migrants Database</h1>
<Link to="/admin/migrants/add">
<Button className="bg-green-700 hover:bg-green-800">
<PlusCircle className="mr-2 size-4" /> Add New Migrant
</Button>
</Link>
</div>
<Card className="mb-6 md:mb-8">
<CardHeader className="pb-3 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 sm:gap-0">
<CardTitle className="text-xl font-serif">Search & Filter</CardTitle>
<Button variant="outline" size="sm">
<Filter className="mr-2 size-4" /> Advanced Filters
</Button>
</CardHeader>
<CardContent>
<div className="grid gap-4 md:grid-cols-3">
<div className="relative">
<Search className="absolute left-3 top-2.5 size-5 text-neutral-500" />
<Input
placeholder="Search migrants..."
className="pl-10 border-neutral-300"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="flex gap-2">
<Button variant="outline" className="flex-1">
<Upload className="mr-2 size-4" /> Import
</Button>
<Button variant="outline" className="flex-1">
<Download className="mr-2 size-4" /> Export
</Button>
</div>
<div className="flex justify-end">
{selectedMigrants.length > 0 && (
<Button variant="destructive" size="sm">
<Trash2 className="mr-2 size-4" /> Delete Selected ({selectedMigrants.length})
</Button>
)}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardContent className="p-0 overflow-auto">
<div className="min-w-[800px]">
<Table>
<TableHeader className="bg-neutral-100">
<TableRow>
<TableHead className="w-12">
<Checkbox
checked={selectedMigrants.length === migrants.length && migrants.length > 0}
onCheckedChange={toggleSelectAll}
aria-label="Select all"
/>
</TableHead>
<TableHead>
<div className="flex items-center">
Name
<ArrowUpDown className="ml-2 size-4" />
</div>
</TableHead>
<TableHead>
<div className="flex items-center">
Birth Date
<ArrowUpDown className="ml-2 size-4" />
</div>
</TableHead>
<TableHead>Birth Place</TableHead>
<TableHead>
<div className="flex items-center">
Arrival Date
<ArrowUpDown className="ml-2 size-4" />
</div>
</TableHead>
<TableHead>Occupation</TableHead>
<TableHead>Photos</TableHead>
<TableHead className="w-12">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredMigrants.length === 0 ? (
<TableRow>
<TableCell colSpan={8} className="text-center py-8 text-neutral-500">
No migrants found matching your search criteria.
</TableCell>
</TableRow>
) : (
filteredMigrants.map((migrant) => (
<TableRow key={migrant.id} className="hover:bg-neutral-50">
<TableCell>
<Checkbox
checked={selectedMigrants.includes(migrant.id)}
onCheckedChange={() => toggleSelectMigrant(migrant.id)}
aria-label={`Select ${migrant.name}`}
/>
</TableCell>
<TableCell className="font-medium">{migrant.name}</TableCell>
<TableCell>{new Date(migrant.birthDate).toLocaleDateString()}</TableCell>
<TableCell>{migrant.birthPlace}</TableCell>
<TableCell>{new Date(migrant.arrivalDate).toLocaleDateString()}</TableCell>
<TableCell>{migrant.occupation}</TableCell>
<TableCell>
{migrant.hasPhotos ? (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Yes
</span>
) : (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-neutral-100 text-neutral-800">
No
</span>
)}
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<MoreHorizontal className="size-4" />
<span className="sr-only">Open menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<Link to={`/admin/migrants/edit/${migrant.id}`}>
<DropdownMenuItem>Edit</DropdownMenuItem>
</Link>
<DropdownMenuItem className="text-red-600">Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</main>
</div>
</div>
)
}

View File

@ -0,0 +1,231 @@
"use client"
import { useState } from "react"
import { motion } from "framer-motion"
import { Eye, EyeOff, Lock, Mail } from "lucide-react"
import { Link, useNavigate } from "react-router-dom"
import apiService from "@/services/apiService"
export default function SimpleForm() {
const [error, setError] = useState("")
const [showPassword, setShowPassword] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [name, setName] = useState("")
const navigate = useNavigate()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)
setError("")
try {
const response = await apiService.register({
name,
email,
password,
})
console.log("Response:", response.data)
alert("Registration successful!")
setName("")
setEmail("")
setPassword("")
} catch (error: any) {
console.error("Error submitting form:", error)
if (error.response && error.response.data && error.response.data.message) {
setError(error.response.data.message)
} else {
setError("Registration failed. Please check your input and try again.")
}
} finally {
setIsLoading(false)
navigate("/login")
}
}
return (
<div className="min-h-screen flex items-center justify-center bg-[#E8DCCA] bg-opacity-30">
<div className="absolute inset-0 overflow-hidden z-0">
<motion.div
className="absolute inset-0 bg-[url('/italian-migrants-historical.jpg')] bg-cover bg-center"
initial={{ opacity: 0 }}
animate={{ opacity: 0.15 }}
transition={{ duration: 1.5 }}
/>
<div className="absolute inset-0 bg-gradient-to-b from-[#9B2335]/20 to-[#01796F]/20" />
</div>
<motion.div
className="max-w-md w-full mx-4 bg-white rounded-lg shadow-xl overflow-hidden z-10"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="h-2 bg-gradient-to-r from-[#9B2335] via-[#E8DCCA] to-[#01796F]" />
<div className="px-8 pt-8 pb-6">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3, duration: 0.6 }}
className="text-center"
>
<h2 className="text-2xl font-serif font-bold text-[#1A2A57]">Admin Access</h2>
<p className="text-gray-600 mt-1 italic">
Northern Territory Italian Migration History
</p>
</motion.div>
</div>
<motion.div
className="px-8 pb-8"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.5, duration: 0.6 }}
>
{error && (
<motion.div
className="mb-4 p-3 bg-red-50 border-l-4 border-[#9B2335] text-[#9B2335]"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
>
{error}
</motion.div>
)}
<form onSubmit={handleSubmit}>
<div className="mb-6">
<label htmlFor="name" className="block text-sm font-medium text-gray-700 mb-1">
Name
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Mail className="h-5 w-5 text-gray-400" />
</div>
<input
id="name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[#01796F] focus:border-[#01796F]"
placeholder="John Doe"
required
/>
</div>
</div>
<div className="mb-6">
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
Email
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Mail className="h-5 w-5 text-gray-400" />
</div>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[#01796F] focus:border-[#01796F]"
placeholder="admin@example.com"
required
/>
</div>
</div>
<div className="mb-6">
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<Lock className="h-5 w-5 text-gray-400" />
</div>
<input
id="password"
type={showPassword ? "text" : "password"}
value={password}
onChange={(e) => setPassword(e.target.value)}
className="block w-full pl-10 pr-10 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-[#01796F] focus:border-[#01796F]"
placeholder="••••••••"
required
/>
<div className="absolute inset-y-0 right-0 pr-3 flex items-center">
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="text-gray-400 hover:text-gray-500 focus:outline-none"
>
{showPassword ? <EyeOff className="h-5 w-5" /> : <Eye className="h-5 w-5" />}
</button>
</div>
</div>
</div>
<motion.button
type="submit"
disabled={isLoading}
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-white bg-[#9B2335] hover:bg-[#9B2335]/90 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#9B2335] disabled:opacity-50 disabled:cursor-not-allowed"
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
>
{isLoading ? (
<div className="flex items-center">
<svg
className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Signing in...
</div>
) : (
"Sign in"
)}
</motion.button>
</form>
<div className="mt-6 flex items-center justify-center">
<div className="h-px bg-gray-300 w-full" />
<span className="px-2 text-sm text-gray-500">or</span>
<div className="h-px bg-gray-300 w-full" />
</div>
<div className="mt-6 text-center">
<Link to="/" className="text-sm font-medium text-[#1A2A57] hover:text-[#1A2A57]/80">
Return to public site
</Link>
</div>
</motion.div>
</motion.div>
<motion.div
className="absolute bottom-4 text-center text-xs text-gray-500"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1, duration: 0.6 }}
>
© {new Date().getFullYear()} Northern Territory Italian Migration History Project
</motion.div>
</div>
)
}

View File

@ -0,0 +1,41 @@
// InterneeDetailsTab.jsx
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
export function InterneeDetailsTab() {
return (
<Card>
<CardHeader>
<CardTitle className="text-xl font-serif">Internee Details</CardTitle>
<CardDescription>Information about internment (if applicable)</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="internedIn">Interned In</Label>
<Input id="internedIn" placeholder="Location of internment" className="border-neutral-300" />
</div>
<div className="space-y-2">
<Label htmlFor="sentTo">Sent To</Label>
<Input id="sentTo" placeholder="Destination after internment" className="border-neutral-300" />
</div>
<div className="space-y-2">
<Label htmlFor="interneeOccupation">Internee Occupation</Label>
<Input id="interneeOccupation" placeholder="Occupation during internment" className="border-neutral-300" />
</div>
<div className="space-y-2">
<Label htmlFor="interneeAddress">Internee Address</Label>
<Textarea
id="interneeAddress"
placeholder="Address during internment"
className="min-h-[5rem] border-neutral-300"
/>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,35 @@
// LocationsTab.jsx
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
export function LocationsTab() {
return (
<Card>
<CardHeader>
<CardTitle className="text-xl font-serif">Location Information</CardTitle>
<CardDescription>Check all applicable locations</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="flex items-center space-x-2">
<Checkbox id="darwin" />
<Label htmlFor="darwin">Darwin</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="katherine" />
<Label htmlFor="katherine">Katherine</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="tennantCreek" />
<Label htmlFor="tennantCreek">Tennant Creek</Label>
</div>
<div className="flex items-center space-x-2">
<Checkbox id="aliceSprings" />
<Label htmlFor="aliceSprings">Alice Springs</Label>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,123 @@
// MigrationDetailsTab.jsx
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Calendar } from "lucide-react";
export function MigrationDetailsTab({ formData, handleInputChange }: {
formData: {
date_of_arrival_australia: string
date_of_arrival_nt: string
date_of_naturalisation: string
corps_issued: string
no_of_cert: string
issued_at: string
}
handleInputChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void
}) {
return (
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-xl font-serif">Migration Details</CardTitle>
<CardDescription>Information about the migration journey</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="date_of_arrival_australia">Date of Arrival in Australia</Label>
<div className="relative">
<Calendar className="absolute left-3 top-2.5 size-5 text-neutral-500" />
<Input
id="date_of_arrival_australia"
value={formData.date_of_arrival_australia}
type="date"
className="pl-10 border-neutral-300"
onChange={handleInputChange}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="date_of_arrival_nt">Date of Arrival in NT *</Label>
<div className="relative">
<Calendar className="absolute left-3 top-2.5 size-5 text-neutral-500" />
<Input
id="date_of_arrival_nt"
value={formData.date_of_arrival_nt}
type="date"
className="pl-10 border-neutral-300"
onChange={handleInputChange}
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="date_of_naturalisation">Date of Naturalisation</Label>
<div className="relative">
<Calendar className="absolute left-3 top-2.5 size-5 text-neutral-500" />
<Input
id="date_of_naturalisation"
value={formData.date_of_naturalisation}
type="date"
className="pl-10 border-neutral-300"
onChange={handleInputChange}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="arrival_period">Arrival Period (auto-generated)</Label>
<Input
id="arrival_period"
placeholder="This will be auto-generated"
className="border-neutral-300 bg-neutral-100"
disabled
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-xl font-serif">Additional Migration Info</CardTitle>
<CardDescription>More details about migration history</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="corps_issued">Corps Issued</Label>
<Input
id="corps_issued"
value={formData.corps_issued}
placeholder="Corps Issued information"
className="border-neutral-300"
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="no_of_cert">Number of Certificate</Label>
<Input
id="no_of_cert"
value={formData.no_of_cert}
placeholder="Certificate number"
className="border-neutral-300"
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="issued_at">Issued At</Label>
<Input
id="issued_at"
value={formData.issued_at}
placeholder="Place of issuance"
className="border-neutral-300"
onChange={handleInputChange}
/>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,25 @@
// NotesTab.jsx
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
export function NotesTab() {
return (
<Card>
<CardHeader>
<CardTitle className="text-xl font-serif">Additional Notes</CardTitle>
<CardDescription>Any other relevant information about this migrant</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="additionalNotes">Additional Notes</Label>
<Textarea
id="additionalNotes"
placeholder="Enter any additional information..."
className="min-h-[8rem] border-neutral-300"
/>
</div>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,198 @@
// PersonalInfoTab.jsx
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Calendar } from "lucide-react";
export function PersonalInfoTab({ formData, handleInputChange }: {
formData: {
surname: string
christian_name: string
full_name: string
date_of_birth: string
date_of_death: string
place_of_birth: string
home_at_death: string
occupation: string
names_of_parents: string
names_of_children: string
data_source: string
reference: string
cav: string
id_card_no: string
}
handleInputChange: (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void
}) {
return (
<div className="grid gap-6 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-xl font-serif">Basic Information</CardTitle>
<CardDescription>Enter the migrant's personal details</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label htmlFor="surname">Surname *</Label>
<Input
id="surname"
value={formData.surname}
placeholder="Surname"
className="border-neutral-300"
required
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="christian_name">Christian Name *</Label>
<Input
id="christian_name"
value={formData.christian_name}
placeholder="Christian Name"
className="border-neutral-300"
required
onChange={handleInputChange}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="date_of_birth">Date of Birth</Label>
<div className="relative">
<Calendar className="absolute left-3 top-2.5 size-5 text-neutral-500" />
<Input
id="date_of_birth"
value={formData.date_of_birth}
type="date"
className="pl-10 border-neutral-300"
onChange={handleInputChange}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="date_of_death">Date of Death</Label>
<div className="relative">
<Calendar className="absolute left-3 top-2.5 size-5 text-neutral-500" />
<Input
id="date_of_death"
value={formData.date_of_death}
type="date"
className="pl-10 border-neutral-300"
onChange={handleInputChange}
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="place_of_birth">Place of Birth</Label>
<Input
id="place_of_birth"
value={formData.place_of_birth}
placeholder="City, Region, Country"
className="border-neutral-300"
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="home_at_death">Home at Death</Label>
<Input
id="home_at_death"
value={formData.home_at_death}
placeholder="Location at time of death"
className="border-neutral-300"
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="occupation">Occupation</Label>
<Input
id="occupation"
value={formData.occupation}
placeholder="Primary occupation"
className="border-neutral-300"
onChange={handleInputChange}
/>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-xl font-serif">Family Information</CardTitle>
<CardDescription>Information about family members</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label htmlFor="names_of_parents">Names of Parents</Label>
<Textarea
id="names_of_parents"
value={formData.names_of_parents}
placeholder="Enter parent names"
className="min-h-[5rem] border-neutral-300"
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="names_of_children">Names of Children</Label>
<Textarea
id="names_of_children"
value={formData.names_of_children}
placeholder="Enter children names"
className="min-h-[5rem] border-neutral-300"
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="data_source">Data Source</Label>
<Input
id="data_source"
value={formData.data_source}
placeholder="Source of information"
className="border-neutral-300"
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="reference">Reference</Label>
<Input
id="reference"
value={formData.reference}
placeholder="Reference information"
className="border-neutral-300"
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="cav">CAV</Label>
<Input
id="cav"
value={formData.cav}
placeholder="CAV information"
className="border-neutral-300"
onChange={handleInputChange}
/>
</div>
<div className="space-y-2">
<Label htmlFor="id_card_no">ID / Card No</Label>
<Input
id="id_card_no"
value={formData.id_card_no}
placeholder="ID or Card Number"
className="border-neutral-300"
onChange={handleInputChange}
/>
</div>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,107 @@
// PhotosTab.jsx
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Label } from "@/components/ui/label";
import { ImageIcon, Upload, X } from "lucide-react";
import { useState } from "react";
export function PhotosTab() {
const [photos, setPhotos] = useState<string[]>([]);
const [mainPhotoIndex, setMainPhotoIndex] = useState<number | null>(null);
// Simulate photo upload
const handlePhotoUpload = () => {
// In a real app, this would handle file upload
const newPhoto = `/placeholder.svg?height=200&width=200`;
setPhotos([...photos, newPhoto]);
if (mainPhotoIndex === null) {
setMainPhotoIndex(photos.length);
}
};
const removePhoto = (index: number) => {
const newPhotos = [...photos];
newPhotos.splice(index, 1);
setPhotos(newPhotos);
if (mainPhotoIndex === index) {
setMainPhotoIndex(newPhotos.length > 0 ? 0 : null);
} else if (mainPhotoIndex !== null && mainPhotoIndex > index) {
setMainPhotoIndex(mainPhotoIndex - 1);
}
};
const setAsMainPhoto = (index: number) => {
setMainPhotoIndex(index);
};
return (
<Card>
<CardHeader>
<CardTitle className="text-xl font-serif">Photos & Documents</CardTitle>
<CardDescription>Upload photos and documents related to this migrant</CardDescription>
</CardHeader>
<CardContent>
<div className="mb-6">
<Button onClick={handlePhotoUpload} className="bg-green-700 hover:bg-green-800">
<Upload className="mr-2 size-4" /> Upload Photos
</Button>
</div>
{photos.length > 0 ? (
<div>
<Label className="mb-2 block">Photos ({photos.length})</Label>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{photos.map((photo, index) => (
<div
key={index}
className={`relative rounded-md border ${
mainPhotoIndex === index ? "border-green-600 ring-2 ring-green-200" : "border-neutral-200"
}`}
>
<img
src={photo || "/placeholder.svg"}
alt={`Migrant photo ${index + 1}`}
className="w-full h-40 object-cover rounded-t-md"
/>
<div className="p-2 bg-neutral-50 rounded-b-md">
<div className="flex justify-between items-center">
{mainPhotoIndex === index ? (
<span className="text-xs font-medium text-green-700">Main Photo</span>
) : (
<Button
variant="ghost"
size="sm"
className="h-7 text-xs"
onClick={() => setAsMainPhoto(index)}
>
<ImageIcon className="mr-1 size-3" /> Set as Main
</Button>
)}
<Button
variant="ghost"
size="sm"
className="h-7 text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => removePhoto(index)}
>
<X className="size-4" />
</Button>
</div>
</div>
</div>
))}
</div>
</div>
) : (
<div className="text-center py-12 border-2 border-dashed border-neutral-200 rounded-md">
<ImageIcon className="mx-auto size-12 text-neutral-400" />
<h3 className="mt-2 text-sm font-medium text-neutral-900">No photos uploaded</h3>
<p className="mt-1 text-sm text-neutral-500">
Upload photos of the migrant or related documents
</p>
</div>
)}
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,25 @@
import type { ReactNode } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
interface StatCardProps {
title: string
value: string
description: string
icon: ReactNode
}
export default function StatCard({ title, value, description, icon }: StatCardProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-neutral-600">{title}</CardTitle>
{icon}
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<CardDescription>{description}</CardDescription>
</CardContent>
</Card>
)
}

View File

@ -0,0 +1,71 @@
import { Edit, FileText, Plus, Trash2, Upload } from "lucide-react"
// Sample activity data
const activities = [
{
id: 1,
action: "added",
subject: "Marco Rossi",
timestamp: "2 hours ago",
icon: Plus,
iconColor: "text-green-600",
user: "Admin",
},
{
id: 2,
action: "updated",
subject: "Sofia Bianchi",
timestamp: "Yesterday",
icon: Edit,
iconColor: "text-blue-600",
user: "Admin",
},
{
id: 3,
action: "uploaded",
subject: "5 photos for Antonio Esposito",
timestamp: "2 days ago",
icon: Upload,
iconColor: "text-purple-600",
user: "Admin",
},
{
id: 4,
action: "deleted",
subject: "Duplicate record",
timestamp: "3 days ago",
icon: Trash2,
iconColor: "text-red-600",
user: "Admin",
},
{
id: 5,
action: "added",
subject: "Document for Lucia Romano",
timestamp: "1 week ago",
icon: FileText,
iconColor: "text-amber-600",
user: "Admin",
},
]
export default function RecentActivityList() {
return (
<div className="divide-y divide-neutral-100">
{activities.map((activity) => (
<div key={activity.id} className="flex items-start gap-4 p-4">
<div className={`rounded-full p-2 ${activity.iconColor} bg-opacity-10`}>
<activity.icon className="size-4" />
</div>
<div className="flex-1 space-y-1">
<p className="text-sm font-medium">
<span className="font-semibold">{activity.user}</span> {activity.action}{" "}
<span className="font-medium text-neutral-900">{activity.subject}</span>
</p>
<p className="text-xs text-neutral-500">{activity.timestamp}</p>
</div>
</div>
))}
</div>
)
}

View File

@ -1,4 +1,4 @@
import { Card, CardContent } from "./ui/card"; import { Card, CardContent } from "@/components/ui/card";
interface HistoricalContextProps { interface HistoricalContextProps {
year: number; year: number;

View File

@ -5,9 +5,9 @@ import { Link } from "react-router-dom";
import { ArrowLeft } from "lucide-react"; import { ArrowLeft } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { Card, CardContent } from "@/components/ui/card";
import PhotoGallery from "@/components/PhotoGallery"; import PhotoGallery from "@/components/home/PhotoGallery";
import RelatedMigrants from "@/components/RelatedMigrants"; import RelatedMigrants from "@/components/home/RelatedMigrants";
import HistoricalContext from "@/components/HistoricalContext"; import HistoricalContext from "@/components/home/HistoricalContext";
import AnimatedImage from "@/components/ui/animated-image"; import AnimatedImage from "@/components/ui/animated-image";
import type { MigrantProfile as MigrantProfileType } from "@/types/migrant"; import type { MigrantProfile as MigrantProfileType } from "@/types/migrant";

View File

@ -1,8 +1,8 @@
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import type { RelatedMigrant } from "../types/migrant"; import type { RelatedMigrant } from "@/types/migrant";
import { Card, CardContent } from "./ui/card"; import { Card, CardContent } from "@/components/ui/card";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
import AnimatedImage from "./ui/animated-image"; import AnimatedImage from "@/components/ui/animated-image";
interface RelatedMigrantsProps { interface RelatedMigrantsProps {
migrants: RelatedMigrant[]; migrants: RelatedMigrant[];

View File

@ -3,8 +3,8 @@
import { useState } from "react"; 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 apiService from "../services/apiService"; import apiService from "@/services/apiService";
import { Loader2 } from "lucide-react"; import { Loader2 } from "lucide-react";
const SearchSection = () => { const SearchSection = () => {

View File

@ -0,0 +1,40 @@
import { Bell, HelpCircle, User } from "lucide-react"
import { Button } from "@/components/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
interface HeaderProps {
title: string
}
export default function Header({ title }: HeaderProps) {
return (
<header className="h-16 border-b border-neutral-200 bg-white flex items-center justify-between px-6">
<h2 className="text-xl font-medium text-neutral-800">{title}</h2>
<div className="flex items-center space-x-4">
<Button variant="ghost" size="icon" className="text-neutral-600">
<HelpCircle className="size-5" />
</Button>
<Button variant="ghost" size="icon" className="text-neutral-600 relative">
<Bell className="size-5" />
<span className="absolute top-1 right-1 size-2 bg-red-500 rounded-full"></span>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full">
<User className="size-5 text-neutral-600" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Logout</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
)
}

View File

@ -0,0 +1,124 @@
import { useState } from "react"
import { BarChart3, FileText, Home, LogOut, Settings, Users } from "lucide-react"
import { Link } from "react-router-dom"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import apiService from "@/services/apiService"
export default function Sidebar() {
const [collapsed, setCollapsed] = useState(false)
const handleLogout = async () => {
try {
await apiService.logout();
alert("Logged out successfully");
setTimeout(() => {
window.location.href = "/login";
}, 1000); // Delay so the alert shows
} catch (err) {
alert("Logout failed. Please try again.");
}
};
return (
<div
className={`bg-white border-r border-neutral-200 h-dvh transition-all duration-300 ${collapsed ? "w-16" : "w-64"}`}
>
<div className="flex flex-col h-full">
<div className="p-4 flex items-center justify-center border-b border-neutral-200">
{!collapsed && (
<div className="flex items-center">
<div className="size-8 rounded-full bg-gradient-to-r from-green-600 via-white to-red-600 flex items-center justify-center mr-2">
<span className="text-sm font-bold text-neutral-800">NT</span>
</div>
<h1 className="text-lg font-serif font-bold text-neutral-800">Italian Migrants</h1>
</div>
)}
{collapsed && (
<div className="size-8 rounded-full bg-gradient-to-r from-green-600 via-white to-red-600 flex items-center justify-center">
<span className="text-sm font-bold text-neutral-800">NT</span>
</div>
)}
</div>
<nav className="flex-1 py-4 overflow-y-auto">
<ul className="space-y-1 px-2">
<li>
<Link to="/admin/ ">
<Button
variant="ghost"
className={`w-full justify-start ${location.pathname === "/admin/" ? "bg-neutral-100 text-neutral-900" : "text-neutral-700"}`}
>
<Home className={`${collapsed ? "mr-0" : "mr-2"} size-5`} />
{!collapsed && <span>Dashboard</span>}
</Button>
</Link>
</li>
<li>
<Link to="/admin/migrants">
<Button
variant="ghost"
className={`w-full justify-start ${location.pathname === "/admin/migrants" ? "bg-neutral-100 text-neutral-900" : "text-neutral-700"}`}
>
<Users className={`${collapsed ? "mr-0" : "mr-2"} size-5`} />
{!collapsed && <span>Migrants</span>}
</Button>
</Link>
</li>
<li>
<Link to="/admin/reports">
<Button
variant="ghost"
className={`w-full justify-start ${location.pathname === "/admin/reports" ? "bg-neutral-100 text-neutral-900" : "text-neutral-700"}`}
>
<BarChart3 className={`${collapsed ? "mr-0" : "mr-2"} size-5`} />
{!collapsed && <span>Reports</span>}
</Button>
</Link>
</li>
<li>
<Link to="/admin/documents">
<Button
variant="ghost"
className={`w-full justify-start ${location.pathname === "/admin/documents" ? "bg-neutral-100 text-neutral-900" : "text-neutral-700"}`}
>
<FileText className={`${collapsed ? "mr-0" : "mr-2"} size-5`} />
{!collapsed && <span>Documents</span>}
</Button>
</Link>
</li>
</ul>
<Separator className="my-4" />
<ul className="space-y-1 px-2">
<li>
<Link to="/admin/settings">
<Button
variant="ghost"
className={`w-full justify-start ${location.pathname === "/admin/settings" ? "bg-neutral-100 text-neutral-900" : "text-neutral-700"}`}
>
<Settings className={`${collapsed ? "mr-0" : "mr-2"} size-5`} />
{!collapsed && <span>Settings</span>}
</Button>
</Link>
</li>
</ul>
</nav>
<div className="p-4 border-t border-neutral-200">
<Button onClick={handleLogout} variant="ghost" className="w-full justify-start text-neutral-700">
<LogOut className={`${collapsed ? "mr-0" : "mr-2"} size-5`} />
{!collapsed && <span>Logout</span>}
</Button>
</div>
<div className="p-2 border-t border-neutral-200">
<Button variant="ghost" size="sm" className="w-full" onClick={() => setCollapsed(!collapsed)}>
{collapsed ? ">>" : "<<"}
</Button>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,30 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@ -0,0 +1,255 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
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 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 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",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-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 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View File

@ -0,0 +1,26 @@
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
function Separator({
className,
orientation = "horizontal",
decorative = true,
...props
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
return (
<SeparatorPrimitive.Root
data-slot="separator-root"
decorative={decorative}
orientation={orientation}
className={cn(
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
className
)}
{...props}
/>
)
}
export { Separator }

View File

@ -0,0 +1,23 @@
import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme()
return (
<Sonner
theme={theme as ToasterProps["theme"]}
className="toaster group"
style={
{
"--normal-bg": "var(--popover)",
"--normal-text": "var(--popover-foreground)",
"--normal-border": "var(--border)",
} as React.CSSProperties
}
{...props}
/>
)
}
export { Toaster }

114
src/components/ui/table.tsx Normal file
View File

@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@ -0,0 +1,64 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,18 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
return (
<textarea
data-slot="textarea"
className={cn(
"border-input placeholder: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 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
{...props}
/>
)
}
export { Textarea }

View File

@ -1,2 +1,120 @@
@import "tailwindcss"; @import "tailwindcss";
@import "tw-animate-css"; @import "tw-animate-css";
@custom-variant dark (&:is(.dark *));
@theme inline {
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
:root {
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.205 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.205 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.922 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.704 0.191 22.216);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.556 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.556 0 0);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}

View File

@ -1,10 +1,10 @@
import { StrictMode } from "react"; import { StrictMode } from 'react'
import { createRoot } from "react-dom/client"; import { createRoot } from 'react-dom/client'
import "./index.css"; import App from './App.tsx'
import App from "./App.tsx"; import './index.css'
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById('root')!).render(
<StrictMode> <StrictMode>
<App /> <App />
</StrictMode> </StrictMode>,
); )

View File

@ -1,8 +1,8 @@
// src/pages/AdminDashboardPage.tsx // src/pages/AdminDashboardPage.tsx
import AdminDashboard from "@/components/AdminDashboard/DashboardLayout"; import DashboardLayout from "@/components/admin/AdminDashboard";
const AdminDashboardPage = () => { const AdminDashboardPage = () => {
return <AdminDashboard />; return <DashboardLayout />;
}; };
export default AdminDashboardPage; export default AdminDashboardPage;

View File

@ -1,6 +1,6 @@
import HeroSection from "../components/HeroSection"; import HeroSection from "../components/home/HeroSection";
import IntroSection from "../components/IntroSection"; import IntroSection from "../components/home/IntroSection";
import SearchSection from "../components/SearchSection"; import SearchSection from "../components/home/SearchSection";
const HomePage = () => { const HomePage = () => {
return ( return (

View File

@ -2,33 +2,13 @@
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 apiService from "../services/apiService"; 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/home/MigrantProfileComponent";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert"; import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
import { AlertCircle, Loader2 } from "lucide-react"; import { AlertCircle, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { calculateAgeAtMigration } from "@/utils/date";
// 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 }>();

View File

@ -1,66 +1,130 @@
import axios from "axios"; import axios from "axios";
import type { SearchResult, SearchParams } from "../types/search"; import type { SearchResult, SearchParams } from "@/types/search";
import type { CreatePersonPayload, Person, PaginatedResponse } from "@/types/api";
// Load base URL from environment // API base URL from environment variables
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL; const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
// Create Axios instance // Axios instance with base config
const api = axios.create({ const api = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}); });
// Request interceptor for logging // Attach token to each request if available
api.interceptors.request.use((request) => { api.interceptors.request.use((config) => {
const fullUrl = `${request.baseURL || ""}${request.url || ""}`; const token = localStorage.getItem("token");
console.log("[API] Request URL:", fullUrl); if (token) {
config.headers.Authorization = `Bearer ${token}`;
if (request.params) {
console.log("[API] Params:", request.params);
} }
return config;
if (request.url?.includes("/migrants/")) {
const id = request.url.split("/").pop();
console.log("[API] Fetching migrant by ID:", id);
}
return request;
}); });
class ApiService { // Handle unauthorized responses globally
/** api.interceptors.response.use(
* Fetch a single migrant record by ID (res) => res,
*/ (err) => {
async getRecordById(id: string): Promise<SearchResult> { if (err.response?.status === 401) {
const res = await api.get(`/api/migrants/${id}`); localStorage.removeItem("token");
return res.data.data; localStorage.removeItem("user");
}
/** window.location.href = "/login";
* Search for people based on filters. }
* Filters out empty and "all" values. return Promise.reject(err);
*/ }
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 []; class ApiService {
async getRecordById(id: string): Promise<SearchResult> {
const { data } = await api.get(`/api/migrants/${id}`);
return data.data;
}
async searchPeople(params: SearchParams): Promise<SearchResult[]> {
const filteredParams = Object.fromEntries(
Object.entries(params).filter(
([_, value]) => value && value !== "all"
)
);
if (Object.keys(filteredParams).length === 0) return [];
try { try {
const res = await api.get("/api/persons/search", { const { data } = await api.get("/api/persons/search", {
params: { ...filtered, exactMatch: "true" }, params: { ...filteredParams, exactMatch: "true" },
}); });
return res.data.success && res.data.data ? res.data.data.data : []; return data.success && data.data ? data.data.data : [];
} catch (err) { } catch (error) {
const message = const message =
err instanceof Error ? err.message : "Unknown error during search"; error instanceof Error ? error.message : "Unknown search error";
throw new Error(`[API] Failed to search people: ${message}`); throw new Error(`[API] Search failed: ${message}`);
} }
} }
async register(params: { name: string; email: string; password: string }) {
const { data } = await api.post("/api/register", params);
return data;
}
async login(params: { email: string; password: string }) {
const { data } = await api.post("/api/login", params);
localStorage.setItem("token", data.token);
localStorage.setItem("user", JSON.stringify(data.user));
return data;
}
async logout() {
try {
const { data } = await api.post("/api/logout");
localStorage.removeItem("token");
localStorage.removeItem("user");
return data;
} catch (error) {
console.error("[logout] Logout failed:", error);
throw error;
}
}
async createPerson(payload: CreatePersonPayload): Promise<Person> {
const { data } = await api.post("/api/persons", payload);
return data.data;
}
// ✅ Updated to support pagination
async getMigrants(page: number = 1): Promise<PaginatedResponse<Person>> {
const { data } = await api.get(`/api/persons?page=${page}`);
if (data.success && data.data) {
return {
data: data.data.data,
meta: data.data.meta,
links: data.data.links,
};
}
return {
data: [],
meta: {
total: 0,
count: 0,
per_page: 10,
current_page: 1,
last_page: 1,
},
links: {
first: null,
last: null,
prev: null,
next: null,
},
};
}
async deleteMigrant(id: string) {
const { data } = await api.delete(`/api/persons/${id}`);
return data;
}
} }
export default new ApiService(); export default new ApiService();

121
src/types/api.ts Normal file
View File

@ -0,0 +1,121 @@
export interface User {
id: string;
name: string;
email: string;
}
// types/person.ts
export interface Migration {
migration_id?: number;
person_id?: number;
date_of_arrival_aus?: string;
date_of_arrival_nt?: string;
arrival_period?: string;
data_source?: string;
created_at?: string;
updated_at?: string;
}
export interface Naturalization {
naturalization_id?: number;
person_id?: number;
date_of_naturalisation?: string;
no_of_cert?: string;
issued_at?: string;
created_at?: string;
updated_at?: string;
}
export interface Residence {
residence_id?: number;
person_id?: number;
town_or_city?: string;
home_at_death?: string;
created_at?: string;
updated_at?: string;
}
export interface Family {
family_id?: number;
person_id?: number;
name?: string;
relationship?: string;
notes?: string;
created_at?: string;
updated_at?: string;
}
export interface Internment {
internment_id?: number;
person_id?: number;
corps_issued?: string;
interned_in?: string;
sent_to?: string;
internee_occupation?: string;
internee_address?: string;
cav?: string;
created_at?: string;
updated_at?: string;
}
export interface Person {
person_id?: number;
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;
created_at?: string;
updated_at?: string;
migration?: Migration | null;
naturalization?: Naturalization | null;
residence?: Residence | null;
family?: Family | null;
internment?: Internment | null;
}
export interface CreatePersonPayload {
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;
// Optional nested objects
migration?: Migration;
naturalization?: Naturalization;
residence?: Residence;
family?: Family;
internment?: Internment;
}
export interface PaginationMeta {
total: number;
count: number;
per_page: number;
current_page: number;
last_page: number;
}
export interface PaginationLinks {
first: string | null;
last: string | null;
prev: string | null;
next: string | null;
}
export interface PaginatedResponse<T> {
data: T[];
meta: PaginationMeta;
links: PaginationLinks;
}

19
src/utils/date.ts Normal file
View File

@ -0,0 +1,19 @@
export 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;
}
};

View File

@ -1,18 +0,0 @@
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
}