updated ui
This commit is contained in:
parent
73db59a56e
commit
a3d3ff99c9
|
|
@ -8,6 +8,8 @@
|
|||
"name": "italian-migrants-nt",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-avatar": "^1.1.9",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-select": "^2.2.4",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
|
|
@ -1067,6 +1069,32 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-avatar": {
|
||||
"version": "1.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.9.tgz",
|
||||
"integrity": "sha512-10tQokfvZdFvnvDkcOJPjm2pWiP8A0R4T83MoD7tb15bC/k2GU7B1YBuzJi8lNQ8V1QqhP8ocNqp27ByZaNagQ==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-context": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.1.2",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.1",
|
||||
"@radix-ui/react-use-is-hydrated": "0.1.0",
|
||||
"@radix-ui/react-use-layout-effect": "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": {
|
||||
"version": "1.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.6.tgz",
|
||||
|
|
@ -1120,6 +1148,41 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-dialog": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.13.tgz",
|
||||
"integrity": "sha512-ARFmqUyhIVS3+riWzwGTe7JLjqwqgnODBUZdqpWar/z1WFs9z76fuOs/2BOWCR+YboRn4/WN9aoaGVwqNRr8VA==",
|
||||
"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-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-portal": "1.1.8",
|
||||
"@radix-ui/react-presence": "1.1.4",
|
||||
"@radix-ui/react-primitive": "2.1.2",
|
||||
"@radix-ui/react-slot": "1.2.2",
|
||||
"@radix-ui/react-use-controllable-state": "1.2.2",
|
||||
"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-direction": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
||||
|
|
@ -1291,6 +1354,29 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-presence": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz",
|
||||
"integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-compose-refs": "1.1.2",
|
||||
"@radix-ui/react-use-layout-effect": "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-primitive": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz",
|
||||
|
|
@ -1438,6 +1524,23 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-is-hydrated": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
|
||||
"integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "^1.5.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
||||
|
|
@ -5232,6 +5335,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
"integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@
|
|||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-avatar": "^1.1.9",
|
||||
"@radix-ui/react-dialog": "^1.1.13",
|
||||
"@radix-ui/react-label": "^2.1.6",
|
||||
"@radix-ui/react-select": "^2.2.4",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 1.8 MiB |
|
|
@ -0,0 +1,2 @@
|
|||
@import "tailwindcss";
|
||||
@import "tw-animate-css";
|
||||
|
|
@ -9,7 +9,7 @@ export default function HeroSection() {
|
|||
<div className="absolute inset-0 bg-black/60 z-10" />
|
||||
<div className="relative h-full w-full">
|
||||
<AnimatedImage
|
||||
src="https://globalboston.bc.edu/wp-content/uploads/2016/07/06_01_012688.jpg"
|
||||
src="/hero.jpg"
|
||||
alt="Historical image of Italian migrants in the Northern Territory"
|
||||
fill
|
||||
/>
|
||||
|
|
@ -32,7 +32,7 @@ export default function HeroSection() {
|
|||
<h1 className="text-4xl md:text-6xl font-bold text-white mb-4 font-serif">
|
||||
Italian Migration to the Northern Territory
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl text-white max-w-3xl italic">
|
||||
<p className="text-xl md:text-2xl text-white max-w-3xl italic text-center mx-auto">
|
||||
Exploring the rich history and cultural legacy of Italian immigrants
|
||||
in Australia's Northern Territory
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { Card, CardContent } from "./ui/card";
|
||||
|
||||
interface HistoricalContextProps {
|
||||
year: number;
|
||||
}
|
||||
|
|
@ -89,9 +91,12 @@ const HistoricalContext = ({ year }: HistoricalContextProps) => {
|
|||
const context = getHistoricalContext(year);
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="p-6">
|
||||
<h2 className="text-xl font-bold mb-2">Historical Context</h2>
|
||||
<Card className="overflow-hidden border border-gray-200 shadow-none rounded-md bg-white">
|
||||
<CardContent>
|
||||
<h2 className="text-xl font-bold mb-2 font-serif text-gray-800">
|
||||
Contesto Storico
|
||||
</h2>
|
||||
<div className="h-1 w-16 bg-gradient-to-r from-green-600 via-white to-red-600 mb-4" />
|
||||
<h3 className="font-medium text-lg mb-2">{context.period}</h3>
|
||||
<p className="text-sm mb-4">{context.description}</p>
|
||||
<h4 className="font-medium text-sm mb-1">Key Events:</h4>
|
||||
|
|
@ -100,8 +105,8 @@ const HistoricalContext = ({ year }: HistoricalContextProps) => {
|
|||
<li key={index}>{event}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,117 +1,180 @@
|
|||
import { Link } from "react-router-dom"
|
||||
import type { MigrantProfile } from "../types/migrant"
|
||||
import PhotoGallery from "./PhotoGallery"
|
||||
import RelatedMigrants from "./RelatedMigrants"
|
||||
import HistoricalContext from "./HistoricalContext"
|
||||
"use client";
|
||||
|
||||
interface MigrantProfileComponentProps {
|
||||
migrant: MigrantProfile
|
||||
import { motion } from "framer-motion";
|
||||
import { Link } from "react-router-dom";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import PhotoGallery from "@/components/PhotoGallery";
|
||||
import RelatedMigrants from "@/components/RelatedMigrants";
|
||||
import HistoricalContext from "@/components/HistoricalContext";
|
||||
import AnimatedImage from "@/components/ui/animated-image";
|
||||
import type { MigrantProfile as MigrantProfileType } from "@/types/migrant";
|
||||
|
||||
interface MigrantProfileProps {
|
||||
migrant: MigrantProfileType;
|
||||
}
|
||||
|
||||
const MigrantProfileComponent = ({ migrant }: MigrantProfileComponentProps) => {
|
||||
export default function MigrantProfile({ migrant }: MigrantProfileProps) {
|
||||
return (
|
||||
<main className="min-h-screen bg-gray-50 pb-16">
|
||||
{/* Hero section with main photo */}
|
||||
<div className="relative w-full h-[40vh] md:h-[50vh] overflow-hidden bg-gray-900">
|
||||
<img
|
||||
src={migrant.mainPhoto || "/placeholder.jpg"}
|
||||
<div className="relative w-full h-[50vh] md:h-[60vh] overflow-hidden bg-gray-900">
|
||||
<AnimatedImage
|
||||
src={migrant.mainPhoto || "/placeholder.svg?height=1080&width=1920"}
|
||||
alt={`${migrant.firstName} ${migrant.lastName}`}
|
||||
className="w-full h-full object-cover opacity-80"
|
||||
fill
|
||||
className="opacity-80"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 to-transparent" />
|
||||
<div className="absolute bottom-0 left-0 right-0 p-6 md:p-10 text-white">
|
||||
<h1 className="text-3xl md:text-5xl font-bold mb-2">
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/80 to-transparent" />
|
||||
<motion.div
|
||||
className="absolute bottom-0 left-0 right-0 p-6 md:p-10 text-white"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.2 }}
|
||||
>
|
||||
<div className="flex space-x-2 mb-4">
|
||||
<div className="h-8 w-3 bg-green-600" />
|
||||
<div className="h-8 w-3 bg-white" />
|
||||
<div className="h-8 w-3 bg-red-600" />
|
||||
</div>
|
||||
<h1 className="text-3xl md:text-5xl font-bold mb-2 font-serif">
|
||||
{migrant.firstName} {migrant.lastName}
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl opacity-90">
|
||||
{migrant.yearOfArrival} • {migrant.regionOfOrigin}, Italy → {migrant.settlementLocation}, NT
|
||||
{migrant.yearOfArrival} • {migrant.regionOfOrigin}, Italy →{" "}
|
||||
{migrant.settlementLocation}, NT
|
||||
</p>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
{/* Back button */}
|
||||
<div className="max-w-6xl mx-auto px-4 md:px-8 mt-6">
|
||||
<Link to="/" className="btn btn-outline flex items-center gap-2 mb-6">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||
</svg>
|
||||
<Button variant="ghost" asChild className="mb-6 hover:bg-gray-100">
|
||||
<Link to="/" className="flex items-center gap-2">
|
||||
<ArrowLeft size={16} />
|
||||
Back to Search
|
||||
</Link>
|
||||
</Button>
|
||||
</motion.div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Main content - 2/3 width on desktop */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
{/* Biographical information */}
|
||||
<div className="card">
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold mb-4">Biographical Information</h2>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.1 }}
|
||||
>
|
||||
<Card className="overflow-hidden border border-gray-200 shadow-none rounded-md bg-white shadow-none rounded-md bg-white">
|
||||
<CardContent className="">
|
||||
<h2 className="text-2xl font-bold mb-4 font-serif text-gray-800">
|
||||
Biographical Information
|
||||
</h2>
|
||||
<div className="h-1 w-20 bg-gradient-to-r from-green-600 via-white to-red-600 mb-6" />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-8">
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">Full Name</h3>
|
||||
<h3 className="text-sm font-medium text-gray-500">
|
||||
Full Name
|
||||
</h3>
|
||||
<p className="text-lg">
|
||||
{migrant.firstName} {migrant.middleName ? migrant.middleName + " " : ""}
|
||||
{migrant.firstName}{" "}
|
||||
{migrant.middleName ? migrant.middleName + " " : ""}
|
||||
{migrant.lastName}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">Birth Date</h3>
|
||||
<p className="text-lg">{migrant.birthDate || "Unknown"}</p>
|
||||
<h3 className="text-sm font-medium text-gray-500">
|
||||
Birth Date
|
||||
</h3>
|
||||
<p className="text-lg">
|
||||
{migrant.birthDate || "Unknown"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">Birth Place</h3>
|
||||
<p className="text-lg">{migrant.birthPlace || "Unknown"}</p>
|
||||
<h3 className="text-sm font-medium text-gray-500">
|
||||
Birth Place
|
||||
</h3>
|
||||
<p className="text-lg">
|
||||
{migrant.birthPlace || "Unknown"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">Age at Migration</h3>
|
||||
<h3 className="text-sm font-medium text-gray-500">
|
||||
Age at Migration
|
||||
</h3>
|
||||
<p className="text-lg">{migrant.ageAtMigration} years</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">Year of Arrival</h3>
|
||||
<h3 className="text-sm font-medium text-gray-500">
|
||||
Year of Arrival
|
||||
</h3>
|
||||
<p className="text-lg">{migrant.yearOfArrival}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">Region of Origin</h3>
|
||||
<h3 className="text-sm font-medium text-gray-500">
|
||||
Region of Origin
|
||||
</h3>
|
||||
<p className="text-lg">{migrant.regionOfOrigin}, Italy</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">Settlement Location</h3>
|
||||
<p className="text-lg">{migrant.settlementLocation}, NT</p>
|
||||
<h3 className="text-sm font-medium text-gray-500">
|
||||
Settlement Location
|
||||
</h3>
|
||||
<p className="text-lg">
|
||||
{migrant.settlementLocation}, NT
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">Occupation</h3>
|
||||
<p className="text-lg">{migrant.occupation || "Unknown"}</p>
|
||||
<h3 className="text-sm font-medium text-gray-500">
|
||||
Occupation
|
||||
</h3>
|
||||
<p className="text-lg">
|
||||
{migrant.occupation || "Unknown"}
|
||||
</p>
|
||||
</div>
|
||||
{migrant.deathDate && (
|
||||
<>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">Date of Death</h3>
|
||||
<h3 className="text-sm font-medium text-gray-500">
|
||||
Date of Death
|
||||
</h3>
|
||||
<p className="text-lg">{migrant.deathDate}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-medium text-gray-500">Place of Death</h3>
|
||||
<p className="text-lg">{migrant.deathPlace || "Unknown"}</p>
|
||||
<h3 className="text-sm font-medium text-gray-500">
|
||||
Place of Death
|
||||
</h3>
|
||||
<p className="text-lg">
|
||||
{migrant.deathPlace || "Unknown"}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* Life story */}
|
||||
{migrant.biography && (
|
||||
<div className="card">
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold mb-4">Life Story</h2>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.2 }}
|
||||
>
|
||||
<Card className="overflow-hidden border border-gray-200 shadow-none rounded-md bg-white">
|
||||
<CardContent className="">
|
||||
<h2 className="text-2xl font-bold mb-4 font-serif text-gray-800">
|
||||
La Storia di Vita
|
||||
</h2>
|
||||
<div className="h-1 w-20 bg-gradient-to-r from-green-600 via-white to-red-600 mb-6" />
|
||||
<div className="prose max-w-none">
|
||||
{migrant.biography.split("\n").map((paragraph, index) => (
|
||||
<p key={index} className="mb-4">
|
||||
|
|
@ -119,28 +182,47 @@ const MigrantProfileComponent = ({ migrant }: MigrantProfileComponentProps) => {
|
|||
</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{/* Photo gallery - only show if there are additional photos */}
|
||||
{migrant.photos && migrant.photos.length > 0 && <PhotoGallery photos={migrant.photos} />}
|
||||
{migrant.photos && migrant.photos.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.3 }}
|
||||
>
|
||||
<PhotoGallery photos={migrant.photos} />
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar - 1/3 width on desktop */}
|
||||
<div className="space-y-8">
|
||||
{/* Historical context */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4 }}
|
||||
>
|
||||
<HistoricalContext year={migrant.yearOfArrival} />
|
||||
</motion.div>
|
||||
|
||||
{/* Related migrants */}
|
||||
{migrant.relatedMigrants && migrant.relatedMigrants.length > 0 && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.5 }}
|
||||
>
|
||||
<RelatedMigrants migrants={migrant.relatedMigrants} />
|
||||
</motion.div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default MigrantProfileComponent
|
||||
|
|
|
|||
|
|
@ -1,140 +1,132 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useState } from "react"
|
||||
import type { Photo } from "../types/migrant"
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { ChevronLeft, ChevronRight, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import AnimatedImage from "@/components/ui/animated-image";
|
||||
import type { Photo } from "@/types/migrant";
|
||||
|
||||
interface PhotoGalleryProps {
|
||||
photos: Photo[]
|
||||
photos: Photo[];
|
||||
}
|
||||
|
||||
const PhotoGallery = ({ photos }: PhotoGalleryProps) => {
|
||||
const [currentPhotoIndex, setCurrentPhotoIndex] = useState<number | null>(null)
|
||||
|
||||
const openModal = (index: number) => {
|
||||
setCurrentPhotoIndex(index)
|
||||
document.body.style.overflow = "hidden"
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
setCurrentPhotoIndex(null)
|
||||
document.body.style.overflow = "auto"
|
||||
}
|
||||
export default function PhotoGallery({ photos }: PhotoGalleryProps) {
|
||||
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
|
||||
const handlePrevious = () => {
|
||||
if (currentPhotoIndex === null) return
|
||||
setCurrentPhotoIndex((prev) => (prev === 0 ? photos.length - 1 : prev - 1))
|
||||
}
|
||||
setCurrentPhotoIndex((prev) => (prev === 0 ? photos.length - 1 : prev - 1));
|
||||
};
|
||||
|
||||
const handleNext = () => {
|
||||
if (currentPhotoIndex === null) return
|
||||
setCurrentPhotoIndex((prev) => (prev === photos.length - 1 ? 0 : prev + 1))
|
||||
}
|
||||
setCurrentPhotoIndex((prev) => (prev === photos.length - 1 ? 0 : prev + 1));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="card">
|
||||
<div className="p-6">
|
||||
<h2 className="text-2xl font-bold mb-4">Photo Gallery</h2>
|
||||
<Card className="overflow-hidden border border-gray-200 shadow-none rounded-md bg-white">
|
||||
<CardContent className="">
|
||||
<h2 className="text-2xl font-bold mb-4 font-serif text-gray-800">
|
||||
Galleria Fotografica
|
||||
</h2>
|
||||
<div className="h-1 w-20 bg-gradient-to-r from-green-600 via-white to-red-600 mb-6" />
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{photos.map((photo, index) => (
|
||||
<button
|
||||
<Dialog
|
||||
key={index}
|
||||
className="relative h-40 w-full rounded-md overflow-hidden border border-gray-200 hover:opacity-90 transition-opacity"
|
||||
onClick={() => openModal(index)}
|
||||
open={isDialogOpen && currentPhotoIndex === index}
|
||||
onOpenChange={setIsDialogOpen}
|
||||
>
|
||||
<img
|
||||
src={photo.url || "/placeholder.jpg"}
|
||||
<DialogTrigger asChild>
|
||||
<motion.button
|
||||
className="relative h-40 w-full rounded-md overflow-hidden border-4 border-white shadow-md hover:shadow-lg transition-shadow"
|
||||
onClick={() => setCurrentPhotoIndex(index)}
|
||||
whileHover={{ scale: 1.03 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<AnimatedImage
|
||||
src={photo.url || "/placeholder.svg"}
|
||||
alt={photo.caption || `Photo ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
fill
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 hover:opacity-100 transition-opacity duration-300" />
|
||||
{photo.caption && (
|
||||
<div className="absolute bottom-0 left-0 right-0 p-2 bg-black/60 text-white text-xs truncate opacity-0 hover:opacity-100 transition-opacity duration-300">
|
||||
{photo.caption}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal */}
|
||||
{currentPhotoIndex !== null && (
|
||||
<div className="fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4">
|
||||
<div className="relative bg-black rounded-lg overflow-hidden max-w-4xl w-full max-h-[90vh]">
|
||||
<button
|
||||
className="absolute top-2 right-2 z-10 text-white bg-black/50 hover:bg-black/70 p-2 rounded-full"
|
||||
onClick={closeModal}
|
||||
)}
|
||||
</motion.button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-4xl p-0 bg-transparent border-none">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={currentPhotoIndex}
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
className="relative bg-black rounded-lg overflow-hidden"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute top-2 right-2 z-10 text-white bg-black/50 hover:bg-black/70"
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
<path d="M18 6L6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="relative h-[70vh] w-full">
|
||||
<img
|
||||
src={photos[currentPhotoIndex].url || "/placeholder.jpg"}
|
||||
alt={photos[currentPhotoIndex].caption || `Photo ${currentPhotoIndex + 1}`}
|
||||
className="w-full h-full object-contain"
|
||||
<X size={20} />
|
||||
</Button>
|
||||
<div className="relative h-[80vh] w-full">
|
||||
<AnimatedImage
|
||||
src={
|
||||
photos[currentPhotoIndex].url || "/placeholder.svg"
|
||||
}
|
||||
alt={
|
||||
photos[currentPhotoIndex].caption ||
|
||||
`Photo ${currentPhotoIndex + 1}`
|
||||
}
|
||||
fill
|
||||
className="object-contain"
|
||||
/>
|
||||
</div>
|
||||
{photos.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 text-white bg-black/50 hover:bg-black/70 p-2 rounded-full"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute left-2 top-1/2 -translate-y-1/2 text-white bg-black/50 hover:bg-black/70"
|
||||
onClick={handlePrevious}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-white bg-black/50 hover:bg-black/70 p-2 rounded-full"
|
||||
<ChevronLeft size={24} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-white bg-black/50 hover:bg-black/70"
|
||||
onClick={handleNext}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<ChevronRight size={24} />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{photos[currentPhotoIndex].caption && (
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-black/70 text-white p-4">
|
||||
<p>{photos[currentPhotoIndex].caption}</p>
|
||||
{photos[currentPhotoIndex].year && (
|
||||
<p className="text-sm text-gray-300">Year: {photos[currentPhotoIndex].year}</p>
|
||||
<p className="text-sm text-gray-300">
|
||||
Year: {photos[currentPhotoIndex].year}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default PhotoGallery
|
||||
|
|
|
|||
|
|
@ -1,41 +1,57 @@
|
|||
import { Link } from "react-router-dom"
|
||||
import type { RelatedMigrant } from "../types/migrant"
|
||||
import { Link } from "react-router-dom";
|
||||
import type { RelatedMigrant } from "../types/migrant";
|
||||
import { Card, CardContent } from "./ui/card";
|
||||
import { motion } from "framer-motion";
|
||||
import AnimatedImage from "./ui/animated-image";
|
||||
|
||||
interface RelatedMigrantsProps {
|
||||
migrants: RelatedMigrant[]
|
||||
migrants: RelatedMigrant[];
|
||||
}
|
||||
|
||||
const RelatedMigrants = ({ migrants }: RelatedMigrantsProps) => {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="p-6">
|
||||
<h2 className="text-xl font-bold mb-4">Related Migrants</h2>
|
||||
<Card className="overflow-hidden border border-gray-200 shadow-none rounded-md bg-white">
|
||||
<CardContent>
|
||||
<h2 className="text-xl font-bold mb-2 font-serif text-gray-800">
|
||||
Migranti Correlati
|
||||
</h2>
|
||||
<div className="h-1 w-16 bg-gradient-to-r from-green-600 via-white to-red-600 mb-4" />
|
||||
<div className="space-y-4">
|
||||
{migrants.map((migrant) => (
|
||||
<Link
|
||||
{migrants.map((migrant, index) => (
|
||||
<motion.div
|
||||
key={migrant.id}
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.1 }}
|
||||
>
|
||||
<Link
|
||||
to={`/migrant/${migrant.id}`}
|
||||
className="flex items-center gap-3 p-2 rounded-md hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<div className="relative h-12 w-12 rounded-full overflow-hidden flex-shrink-0">
|
||||
<img
|
||||
src={migrant.photoUrl || "/placeholder.jpg"}
|
||||
<div className="relative h-12 w-12 rounded-full overflow-hidden flex-shrink-0 border-2 border-white shadow-sm">
|
||||
<AnimatedImage
|
||||
src={
|
||||
migrant.photoUrl || "/placeholder?height=100&width=100"
|
||||
}
|
||||
alt={`${migrant.firstName} ${migrant.lastName}`}
|
||||
className="w-full h-full object-cover"
|
||||
fill
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-medium">
|
||||
{migrant.firstName} {migrant.lastName}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500">{migrant.relationship}</p>
|
||||
<p className="text-sm text-gray-500">
|
||||
{migrant.relationship}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default RelatedMigrants
|
||||
export default RelatedMigrants;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { useState, type ChangeEvent, type FormEvent } from "react";
|
||||
import type { SearchParams } from "../types/search";
|
||||
import type { SearchParams } from "@/types/search";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Search } from "lucide-react";
|
||||
|
||||
// Mock data for autocomplete
|
||||
const ITALIAN_REGIONS = [
|
||||
|
|
@ -192,19 +194,20 @@ const SearchForm = ({ onSearch }: SearchFormProps) => {
|
|||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-4 mt-8 justify-end">
|
||||
<button
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleReset}
|
||||
className="border border-gray-400 text-gray-700 px-4 py-2 rounded hover:bg-gray-100 transition"
|
||||
className="border-gray-300 text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
<button
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 transition"
|
||||
className="bg-gradient-to-r from-green-600 via-white to-red-600 text-gray-800 hover:from-green-700 hover:via-gray-100 hover:to-red-700 font-medium"
|
||||
>
|
||||
Search Records
|
||||
</button>
|
||||
<Search className="mr-2 h-4 w-4" /> Search Records
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,67 +1,152 @@
|
|||
import { Link } from "react-router-dom"
|
||||
import type { SearchResult } from "../types/search"
|
||||
import LoadingSpinner from "./LoadingSpinner"
|
||||
"use client";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import type { SearchResult } from "@/types/search";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import AnimatedImage from "@/components/ui/animated-image";
|
||||
|
||||
interface SearchResultsProps {
|
||||
results: SearchResult[]
|
||||
isLoading: boolean
|
||||
results: SearchResult[];
|
||||
isLoading: boolean;
|
||||
hasSearched?: boolean;
|
||||
}
|
||||
|
||||
const SearchResults = ({ results, isLoading }: SearchResultsProps) => {
|
||||
export default function SearchResults({
|
||||
results,
|
||||
isLoading,
|
||||
hasSearched = false,
|
||||
}: SearchResultsProps) {
|
||||
const container = {
|
||||
hidden: { opacity: 0 },
|
||||
show: {
|
||||
opacity: 1,
|
||||
transition: {
|
||||
staggerChildren: 0.1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const item = {
|
||||
hidden: { opacity: 0, y: 20 },
|
||||
show: { opacity: 1, y: 0, transition: { duration: 0.5 } },
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<LoadingSpinner />
|
||||
<p className="mt-4 text-gray-500">Searching records...</p>
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold mb-6 font-serif">
|
||||
Search Results
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{[...Array(6)].map((_, i) => (
|
||||
<Card
|
||||
key={i}
|
||||
className="overflow-hidden border border-gray-200 shadow-none rounded-md bg-white"
|
||||
>
|
||||
<div className="relative h-48 w-full">
|
||||
<Skeleton className="h-full w-full" />
|
||||
</div>
|
||||
)
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-3/4 mb-2" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-4 w-full mb-2" />
|
||||
<Skeleton className="h-4 w-full mb-2" />
|
||||
<Skeleton className="h-4 w-3/4" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.length === 0 && hasSearched) {
|
||||
return (
|
||||
<motion.div
|
||||
className="text-center py-12 bg-gray-50 rounded-lg border border-gray-200"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<h3 className="text-2xl font-semibold mb-4 font-serif">
|
||||
No Results Found
|
||||
</h3>
|
||||
<p className="text-gray-500">
|
||||
Try adjusting your search criteria to find more records.
|
||||
</p>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
if (results.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<h3 className="text-2xl font-semibold mb-4">No Results Found</h3>
|
||||
<p className="text-gray-500">Try adjusting your search criteria to find more records.</p>
|
||||
</div>
|
||||
)
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3 className="text-2xl font-semibold mb-6">Search Results ({results.length})</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
<h3 className="text-2xl font-semibold mb-6 font-serif">
|
||||
Search Results ({results.length})
|
||||
</h3>
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
|
||||
variants={container}
|
||||
initial="hidden"
|
||||
animate="show"
|
||||
>
|
||||
{results.map((person) => (
|
||||
<Link key={person.id} to={`/migrant/${person.id}`} className="block">
|
||||
<div className="card h-full hover:shadow-lg transition-shadow">
|
||||
<div className="relative h-48 w-full">
|
||||
<img
|
||||
src={person.photoUrl || "/placeholder.jpg"}
|
||||
<motion.div key={person.id} variants={item}>
|
||||
<Link to={`/migrant/${person.id}`} className="block h-full">
|
||||
<Card className="overflow-hidden hover:shadow-lg transition-shadow h-full border border-gray-200 group">
|
||||
<div className="relative h-48 w-full overflow-hidden">
|
||||
<AnimatedImage
|
||||
src={
|
||||
person.photoUrl ||
|
||||
"/placeholder.svg?height=300&width=300"
|
||||
}
|
||||
alt={`${person.firstName} ${person.lastName}`}
|
||||
className="w-full h-full object-cover"
|
||||
fill
|
||||
/>
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||
<div className="absolute bottom-0 left-0 right-0 p-3 transform translate-y-full group-hover:translate-y-0 transition-transform duration-300">
|
||||
<div className="flex space-x-1">
|
||||
<div className="h-6 w-2 bg-green-600" />
|
||||
<div className="h-6 w-2 bg-white" />
|
||||
<div className="h-6 w-2 bg-red-600" />
|
||||
</div>
|
||||
<div className="p-4">
|
||||
<h4 className="text-xl font-semibold">
|
||||
</div>
|
||||
</div>
|
||||
<CardHeader>
|
||||
<CardTitle className="font-serif">
|
||||
{person.firstName} {person.lastName}
|
||||
</h4>
|
||||
</CardTitle>
|
||||
<p className="text-sm text-gray-500">
|
||||
Arrived {person.yearOfArrival} at age {person.ageAtMigration}
|
||||
Arrived {person.yearOfArrival} at age{" "}
|
||||
{person.ageAtMigration}
|
||||
</p>
|
||||
<div className="mt-4 space-y-2">
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
<span className="font-medium">From:</span> {person.regionOfOrigin}, Italy
|
||||
<span className="font-medium">From:</span>{" "}
|
||||
{person.regionOfOrigin}, Italy
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-medium">Settled in:</span> {person.settlementLocation}, NT
|
||||
<span className="font-medium">Settled in:</span>{" "}
|
||||
{person.settlementLocation}, NT
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default SearchResults
|
||||
|
|
|
|||
|
|
@ -1,39 +1,43 @@
|
|||
"use client"
|
||||
"use client";
|
||||
|
||||
import { useState } from "react"
|
||||
import SearchForm from "./SearchForm"
|
||||
import SearchResults from "./SearchResults"
|
||||
import type { SearchParams, SearchResult } from "../types/search"
|
||||
import { searchMigrants } from "../services/migrantService"
|
||||
import { useState } from "react";
|
||||
import SearchForm from "./SearchForm";
|
||||
import SearchResults from "./SearchResults";
|
||||
import type { SearchParams, SearchResult } from "../types/search";
|
||||
import { searchMigrants } from "../services/migrantService";
|
||||
|
||||
const SearchSection = () => {
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([])
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [hasSearched, setHasSearched] = useState(false)
|
||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [hasSearched, setHasSearched] = useState(false);
|
||||
|
||||
const handleSearch = async (params: SearchParams) => {
|
||||
setIsSearching(true)
|
||||
setIsSearching(true);
|
||||
try {
|
||||
const results = await searchMigrants(params)
|
||||
setSearchResults(results)
|
||||
setHasSearched(true)
|
||||
const results = await searchMigrants(params);
|
||||
setSearchResults(results);
|
||||
setHasSearched(true);
|
||||
} catch (error) {
|
||||
console.error("Error searching migrants:", error)
|
||||
console.error("Error searching migrants:", error);
|
||||
// In a real application, you would handle this error more gracefully
|
||||
} finally {
|
||||
setIsSearching(false)
|
||||
}
|
||||
setIsSearching(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="py-16 px-4 md:px-8 bg-gray-50">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-center">Search Historical Records</h2>
|
||||
<h2 className="text-3xl md:text-4xl font-bold mb-8 text-center">
|
||||
Search Historical Records
|
||||
</h2>
|
||||
<SearchForm onSearch={handleSearch} />
|
||||
{(isSearching || hasSearched) && <SearchResults results={searchResults} isLoading={isSearching} />}
|
||||
{(isSearching || hasSearched) && (
|
||||
<SearchResults results={searchResults} isLoading={isSearching} />
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchSection
|
||||
export default SearchSection;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback }
|
||||
|
|
@ -75,7 +75,7 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
|
||||
className={cn("flex items-center px-6 [.border-t]:", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,133 @@
|
|||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background 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 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-accent animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Skeleton }
|
||||
14
src/main.tsx
14
src/main.tsx
|
|
@ -1,10 +1,10 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./index.css";
|
||||
import App from "./App.tsx";
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
</StrictMode>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
import HeroSection from "../components/HeroSection"
|
||||
import IntroSection from "../components/IntroSection"
|
||||
import SearchSection from "../components/SearchSection"
|
||||
import HeroSection from "../components/HeroSection";
|
||||
import IntroSection from "../components/IntroSection";
|
||||
import SearchSection from "../components/SearchSection";
|
||||
|
||||
const HomePage = () => {
|
||||
return (
|
||||
<main>
|
||||
<HeroSection />
|
||||
<IntroSection />
|
||||
<SearchSection />
|
||||
<IntroSection />
|
||||
</main>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage
|
||||
export default HomePage;
|
||||
|
|
|
|||
Loading…
Reference in New Issue