updated ui
This commit is contained in:
parent
73db59a56e
commit
a3d3ff99c9
|
|
@ -8,6 +8,8 @@
|
||||||
"name": "italian-migrants-nt",
|
"name": "italian-migrants-nt",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"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-label": "^2.1.6",
|
||||||
"@radix-ui/react-select": "^2.2.4",
|
"@radix-ui/react-select": "^2.2.4",
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@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": {
|
"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",
|
||||||
|
|
@ -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": {
|
"node_modules/@radix-ui/react-direction": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-primitive": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.2.tgz",
|
"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": {
|
"node_modules/@radix-ui/react-use-layout-effect": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
|
"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": {
|
"node_modules/vary": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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-label": "^2.1.6",
|
||||||
"@radix-ui/react-select": "^2.2.4",
|
"@radix-ui/react-select": "^2.2.4",
|
||||||
"@radix-ui/react-slot": "^1.2.2",
|
"@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="absolute inset-0 bg-black/60 z-10" />
|
||||||
<div className="relative h-full w-full">
|
<div className="relative h-full w-full">
|
||||||
<AnimatedImage
|
<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"
|
alt="Historical image of Italian migrants in the Northern Territory"
|
||||||
fill
|
fill
|
||||||
/>
|
/>
|
||||||
|
|
@ -32,7 +32,7 @@ export default function HeroSection() {
|
||||||
<h1 className="text-4xl md:text-6xl font-bold text-white mb-4 font-serif">
|
<h1 className="text-4xl md:text-6xl font-bold text-white mb-4 font-serif">
|
||||||
Italian Migration to the Northern Territory
|
Italian Migration to the Northern Territory
|
||||||
</h1>
|
</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
|
Exploring the rich history and cultural legacy of Italian immigrants
|
||||||
in Australia's Northern Territory
|
in Australia's Northern Territory
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Card, CardContent } from "./ui/card";
|
||||||
|
|
||||||
interface HistoricalContextProps {
|
interface HistoricalContextProps {
|
||||||
year: number;
|
year: number;
|
||||||
}
|
}
|
||||||
|
|
@ -89,9 +91,12 @@ const HistoricalContext = ({ year }: HistoricalContextProps) => {
|
||||||
const context = getHistoricalContext(year);
|
const context = getHistoricalContext(year);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<Card className="overflow-hidden border border-gray-200 shadow-none rounded-md bg-white">
|
||||||
<div className="p-6">
|
<CardContent>
|
||||||
<h2 className="text-xl font-bold mb-2">Historical Context</h2>
|
<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>
|
<h3 className="font-medium text-lg mb-2">{context.period}</h3>
|
||||||
<p className="text-sm mb-4">{context.description}</p>
|
<p className="text-sm mb-4">{context.description}</p>
|
||||||
<h4 className="font-medium text-sm mb-1">Key Events:</h4>
|
<h4 className="font-medium text-sm mb-1">Key Events:</h4>
|
||||||
|
|
@ -100,8 +105,8 @@ const HistoricalContext = ({ year }: HistoricalContextProps) => {
|
||||||
<li key={index}>{event}</li>
|
<li key={index}>{event}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,117 +1,180 @@
|
||||||
import { Link } from "react-router-dom"
|
"use client";
|
||||||
import type { MigrantProfile } from "../types/migrant"
|
|
||||||
import PhotoGallery from "./PhotoGallery"
|
|
||||||
import RelatedMigrants from "./RelatedMigrants"
|
|
||||||
import HistoricalContext from "./HistoricalContext"
|
|
||||||
|
|
||||||
interface MigrantProfileComponentProps {
|
import { motion } from "framer-motion";
|
||||||
migrant: MigrantProfile
|
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 (
|
return (
|
||||||
<main className="min-h-screen bg-gray-50 pb-16">
|
<main className="min-h-screen bg-gray-50 pb-16">
|
||||||
{/* Hero section with main photo */}
|
{/* Hero section with main photo */}
|
||||||
<div className="relative w-full h-[40vh] md:h-[50vh] overflow-hidden bg-gray-900">
|
<div className="relative w-full h-[50vh] md:h-[60vh] overflow-hidden bg-gray-900">
|
||||||
<img
|
<AnimatedImage
|
||||||
src={migrant.mainPhoto || "/placeholder.jpg"}
|
src={migrant.mainPhoto || "/placeholder.svg?height=1080&width=1920"}
|
||||||
alt={`${migrant.firstName} ${migrant.lastName}`}
|
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 inset-0 bg-gradient-to-t from-black/80 to-transparent" />
|
||||||
<div className="absolute bottom-0 left-0 right-0 p-6 md:p-10 text-white">
|
<motion.div
|
||||||
<h1 className="text-3xl md:text-5xl font-bold mb-2">
|
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}
|
{migrant.firstName} {migrant.lastName}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl md:text-2xl opacity-90">
|
<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>
|
</p>
|
||||||
</div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Back button */}
|
{/* Back button */}
|
||||||
<div className="max-w-6xl mx-auto px-4 md:px-8 mt-6">
|
<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">
|
<motion.div
|
||||||
<svg
|
initial={{ opacity: 0, x: -20 }}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
animate={{ opacity: 1, x: 0 }}
|
||||||
width="16"
|
transition={{ duration: 0.5 }}
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
>
|
||||||
<path d="M19 12H5M12 19l-7-7 7-7" />
|
<Button variant="ghost" asChild className="mb-6 hover:bg-gray-100">
|
||||||
</svg>
|
<Link to="/" className="flex items-center gap-2">
|
||||||
|
<ArrowLeft size={16} />
|
||||||
Back to Search
|
Back to Search
|
||||||
</Link>
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
{/* Main content - 2/3 width on desktop */}
|
{/* Main content - 2/3 width on desktop */}
|
||||||
<div className="lg:col-span-2 space-y-8">
|
<div className="lg:col-span-2 space-y-8">
|
||||||
{/* Biographical information */}
|
{/* Biographical information */}
|
||||||
<div className="card">
|
<motion.div
|
||||||
<div className="p-6">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<h2 className="text-2xl font-bold mb-4">Biographical Information</h2>
|
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 className="grid grid-cols-1 md:grid-cols-2 gap-y-4 gap-x-8">
|
||||||
<div>
|
<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">
|
<p className="text-lg">
|
||||||
{migrant.firstName} {migrant.middleName ? migrant.middleName + " " : ""}
|
{migrant.firstName}{" "}
|
||||||
|
{migrant.middleName ? migrant.middleName + " " : ""}
|
||||||
{migrant.lastName}
|
{migrant.lastName}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-500">Birth Date</h3>
|
<h3 className="text-sm font-medium text-gray-500">
|
||||||
<p className="text-lg">{migrant.birthDate || "Unknown"}</p>
|
Birth Date
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg">
|
||||||
|
{migrant.birthDate || "Unknown"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-500">Birth Place</h3>
|
<h3 className="text-sm font-medium text-gray-500">
|
||||||
<p className="text-lg">{migrant.birthPlace || "Unknown"}</p>
|
Birth Place
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg">
|
||||||
|
{migrant.birthPlace || "Unknown"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<p className="text-lg">{migrant.ageAtMigration} years</p>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<p className="text-lg">{migrant.yearOfArrival}</p>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
<p className="text-lg">{migrant.regionOfOrigin}, Italy</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-500">Settlement Location</h3>
|
<h3 className="text-sm font-medium text-gray-500">
|
||||||
<p className="text-lg">{migrant.settlementLocation}, NT</p>
|
Settlement Location
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg">
|
||||||
|
{migrant.settlementLocation}, NT
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-500">Occupation</h3>
|
<h3 className="text-sm font-medium text-gray-500">
|
||||||
<p className="text-lg">{migrant.occupation || "Unknown"}</p>
|
Occupation
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg">
|
||||||
|
{migrant.occupation || "Unknown"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{migrant.deathDate && (
|
{migrant.deathDate && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<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>
|
<p className="text-lg">{migrant.deathDate}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-medium text-gray-500">Place of Death</h3>
|
<h3 className="text-sm font-medium text-gray-500">
|
||||||
<p className="text-lg">{migrant.deathPlace || "Unknown"}</p>
|
Place of Death
|
||||||
|
</h3>
|
||||||
|
<p className="text-lg">
|
||||||
|
{migrant.deathPlace || "Unknown"}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* Life story */}
|
{/* Life story */}
|
||||||
{migrant.biography && (
|
{migrant.biography && (
|
||||||
<div className="card">
|
<motion.div
|
||||||
<div className="p-6">
|
initial={{ opacity: 0, y: 20 }}
|
||||||
<h2 className="text-2xl font-bold mb-4">Life Story</h2>
|
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">
|
<div className="prose max-w-none">
|
||||||
{migrant.biography.split("\n").map((paragraph, index) => (
|
{migrant.biography.split("\n").map((paragraph, index) => (
|
||||||
<p key={index} className="mb-4">
|
<p key={index} className="mb-4">
|
||||||
|
|
@ -119,28 +182,47 @@ const MigrantProfileComponent = ({ migrant }: MigrantProfileComponentProps) => {
|
||||||
</p>
|
</p>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Photo gallery - only show if there are additional photos */}
|
{/* 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>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar - 1/3 width on desktop */}
|
{/* Sidebar - 1/3 width on desktop */}
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Historical context */}
|
{/* 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} />
|
<HistoricalContext year={migrant.yearOfArrival} />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{/* Related migrants */}
|
{/* Related migrants */}
|
||||||
{migrant.relatedMigrants && migrant.relatedMigrants.length > 0 && (
|
{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} />
|
<RelatedMigrants migrants={migrant.relatedMigrants} />
|
||||||
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MigrantProfileComponent
|
|
||||||
|
|
|
||||||
|
|
@ -1,140 +1,132 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react";
|
||||||
import type { Photo } from "../types/migrant"
|
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 {
|
interface PhotoGalleryProps {
|
||||||
photos: Photo[]
|
photos: Photo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const PhotoGallery = ({ photos }: PhotoGalleryProps) => {
|
export default function PhotoGallery({ photos }: PhotoGalleryProps) {
|
||||||
const [currentPhotoIndex, setCurrentPhotoIndex] = useState<number | null>(null)
|
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
const openModal = (index: number) => {
|
|
||||||
setCurrentPhotoIndex(index)
|
|
||||||
document.body.style.overflow = "hidden"
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeModal = () => {
|
|
||||||
setCurrentPhotoIndex(null)
|
|
||||||
document.body.style.overflow = "auto"
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePrevious = () => {
|
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 = () => {
|
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 (
|
return (
|
||||||
<>
|
<Card className="overflow-hidden border border-gray-200 shadow-none rounded-md bg-white">
|
||||||
<div className="card">
|
<CardContent className="">
|
||||||
<div className="p-6">
|
<h2 className="text-2xl font-bold mb-4 font-serif text-gray-800">
|
||||||
<h2 className="text-2xl font-bold mb-4">Photo Gallery</h2>
|
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">
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
{photos.map((photo, index) => (
|
{photos.map((photo, index) => (
|
||||||
<button
|
<Dialog
|
||||||
key={index}
|
key={index}
|
||||||
className="relative h-40 w-full rounded-md overflow-hidden border border-gray-200 hover:opacity-90 transition-opacity"
|
open={isDialogOpen && currentPhotoIndex === index}
|
||||||
onClick={() => openModal(index)}
|
onOpenChange={setIsDialogOpen}
|
||||||
>
|
>
|
||||||
<img
|
<DialogTrigger asChild>
|
||||||
src={photo.url || "/placeholder.jpg"}
|
<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}`}
|
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>
|
)}
|
||||||
</div>
|
</motion.button>
|
||||||
|
</DialogTrigger>
|
||||||
{/* Modal */}
|
<DialogContent className="max-w-4xl p-0 bg-transparent border-none">
|
||||||
{currentPhotoIndex !== null && (
|
<AnimatePresence mode="wait">
|
||||||
<div className="fixed inset-0 bg-black/80 z-50 flex items-center justify-center p-4">
|
<motion.div
|
||||||
<div className="relative bg-black rounded-lg overflow-hidden max-w-4xl w-full max-h-[90vh]">
|
key={currentPhotoIndex}
|
||||||
<button
|
initial={{ opacity: 0 }}
|
||||||
className="absolute top-2 right-2 z-10 text-white bg-black/50 hover:bg-black/70 p-2 rounded-full"
|
animate={{ opacity: 1 }}
|
||||||
onClick={closeModal}
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="relative bg-black rounded-lg overflow-hidden"
|
||||||
>
|
>
|
||||||
<svg
|
<Button
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
variant="ghost"
|
||||||
width="24"
|
size="icon"
|
||||||
height="24"
|
className="absolute top-2 right-2 z-10 text-white bg-black/50 hover:bg-black/70"
|
||||||
viewBox="0 0 24 24"
|
onClick={() => setIsDialogOpen(false)}
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
>
|
||||||
<path d="M18 6L6 18M6 6l12 12" />
|
<X size={20} />
|
||||||
</svg>
|
</Button>
|
||||||
</button>
|
<div className="relative h-[80vh] w-full">
|
||||||
<div className="relative h-[70vh] w-full">
|
<AnimatedImage
|
||||||
<img
|
src={
|
||||||
src={photos[currentPhotoIndex].url || "/placeholder.jpg"}
|
photos[currentPhotoIndex].url || "/placeholder.svg"
|
||||||
alt={photos[currentPhotoIndex].caption || `Photo ${currentPhotoIndex + 1}`}
|
}
|
||||||
className="w-full h-full object-contain"
|
alt={
|
||||||
|
photos[currentPhotoIndex].caption ||
|
||||||
|
`Photo ${currentPhotoIndex + 1}`
|
||||||
|
}
|
||||||
|
fill
|
||||||
|
className="object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{photos.length > 1 && (
|
{photos.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<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"
|
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}
|
onClick={handlePrevious}
|
||||||
>
|
>
|
||||||
<svg
|
<ChevronLeft size={24} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</Button>
|
||||||
width="24"
|
<Button
|
||||||
height="24"
|
variant="ghost"
|
||||||
viewBox="0 0 24 24"
|
size="icon"
|
||||||
fill="none"
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-white bg-black/50 hover:bg-black/70"
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M15 18l-6-6 6-6" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="absolute right-2 top-1/2 -translate-y-1/2 text-white bg-black/50 hover:bg-black/70 p-2 rounded-full"
|
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
>
|
>
|
||||||
<svg
|
<ChevronRight size={24} />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
</Button>
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M9 18l6-6-6-6" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{photos[currentPhotoIndex].caption && (
|
{photos[currentPhotoIndex].caption && (
|
||||||
<div className="absolute bottom-0 left-0 right-0 bg-black/70 text-white p-4">
|
<div className="absolute bottom-0 left-0 right-0 bg-black/70 text-white p-4">
|
||||||
<p>{photos[currentPhotoIndex].caption}</p>
|
<p>{photos[currentPhotoIndex].caption}</p>
|
||||||
{photos[currentPhotoIndex].year && (
|
{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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
)}
|
</Card>
|
||||||
</>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PhotoGallery
|
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,57 @@
|
||||||
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 { motion } from "framer-motion";
|
||||||
|
import AnimatedImage from "./ui/animated-image";
|
||||||
|
|
||||||
interface RelatedMigrantsProps {
|
interface RelatedMigrantsProps {
|
||||||
migrants: RelatedMigrant[]
|
migrants: RelatedMigrant[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const RelatedMigrants = ({ migrants }: RelatedMigrantsProps) => {
|
const RelatedMigrants = ({ migrants }: RelatedMigrantsProps) => {
|
||||||
return (
|
return (
|
||||||
<div className="card">
|
<Card className="overflow-hidden border border-gray-200 shadow-none rounded-md bg-white">
|
||||||
<div className="p-6">
|
<CardContent>
|
||||||
<h2 className="text-xl font-bold mb-4">Related Migrants</h2>
|
<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">
|
<div className="space-y-4">
|
||||||
{migrants.map((migrant) => (
|
{migrants.map((migrant, index) => (
|
||||||
<Link
|
<motion.div
|
||||||
key={migrant.id}
|
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}`}
|
to={`/migrant/${migrant.id}`}
|
||||||
className="flex items-center gap-3 p-2 rounded-md hover:bg-gray-100 transition-colors"
|
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">
|
<div className="relative h-12 w-12 rounded-full overflow-hidden flex-shrink-0 border-2 border-white shadow-sm">
|
||||||
<img
|
<AnimatedImage
|
||||||
src={migrant.photoUrl || "/placeholder.jpg"}
|
src={
|
||||||
|
migrant.photoUrl || "/placeholder?height=100&width=100"
|
||||||
|
}
|
||||||
alt={`${migrant.firstName} ${migrant.lastName}`}
|
alt={`${migrant.firstName} ${migrant.lastName}`}
|
||||||
className="w-full h-full object-cover"
|
fill
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-medium">
|
<h3 className="font-medium">
|
||||||
{migrant.firstName} {migrant.lastName}
|
{migrant.firstName} {migrant.lastName}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-sm text-gray-500">{migrant.relationship}</p>
|
<p className="text-sm text-gray-500">
|
||||||
|
{migrant.relationship}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default RelatedMigrants
|
export default RelatedMigrants;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, type ChangeEvent, type FormEvent } from "react";
|
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
|
// Mock data for autocomplete
|
||||||
const ITALIAN_REGIONS = [
|
const ITALIAN_REGIONS = [
|
||||||
|
|
@ -192,19 +194,20 @@ const SearchForm = ({ onSearch }: SearchFormProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-4 mt-8 justify-end">
|
<div className="flex flex-wrap gap-4 mt-8 justify-end">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
variant="outline"
|
||||||
onClick={handleReset}
|
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
|
Reset
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="submit"
|
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
|
<Search className="mr-2 h-4 w-4" /> Search Records
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,67 +1,152 @@
|
||||||
import { Link } from "react-router-dom"
|
"use client";
|
||||||
import type { SearchResult } from "../types/search"
|
|
||||||
import LoadingSpinner from "./LoadingSpinner"
|
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 {
|
interface SearchResultsProps {
|
||||||
results: SearchResult[]
|
results: SearchResult[];
|
||||||
isLoading: boolean
|
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) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="text-center py-12">
|
<div>
|
||||||
<LoadingSpinner />
|
<h3 className="text-2xl font-semibold mb-6 font-serif">
|
||||||
<p className="mt-4 text-gray-500">Searching records...</p>
|
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>
|
</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) {
|
if (results.length === 0) {
|
||||||
return (
|
return null;
|
||||||
<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 (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-semibold mb-6">Search Results ({results.length})</h3>
|
<h3 className="text-2xl font-semibold mb-6 font-serif">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
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) => (
|
{results.map((person) => (
|
||||||
<Link key={person.id} to={`/migrant/${person.id}`} className="block">
|
<motion.div key={person.id} variants={item}>
|
||||||
<div className="card h-full hover:shadow-lg transition-shadow">
|
<Link to={`/migrant/${person.id}`} className="block h-full">
|
||||||
<div className="relative h-48 w-full">
|
<Card className="overflow-hidden hover:shadow-lg transition-shadow h-full border border-gray-200 group">
|
||||||
<img
|
<div className="relative h-48 w-full overflow-hidden">
|
||||||
src={person.photoUrl || "/placeholder.jpg"}
|
<AnimatedImage
|
||||||
|
src={
|
||||||
|
person.photoUrl ||
|
||||||
|
"/placeholder.svg?height=300&width=300"
|
||||||
|
}
|
||||||
alt={`${person.firstName} ${person.lastName}`}
|
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>
|
||||||
<div className="p-4">
|
</div>
|
||||||
<h4 className="text-xl font-semibold">
|
</div>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="font-serif">
|
||||||
{person.firstName} {person.lastName}
|
{person.firstName} {person.lastName}
|
||||||
</h4>
|
</CardTitle>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Arrived {person.yearOfArrival} at age {person.ageAtMigration}
|
Arrived {person.yearOfArrival} at age{" "}
|
||||||
|
{person.ageAtMigration}
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4 space-y-2">
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-2">
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">From:</span> {person.regionOfOrigin}, Italy
|
<span className="font-medium">From:</span>{" "}
|
||||||
|
{person.regionOfOrigin}, Italy
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<span className="font-medium">Settled in:</span> {person.settlementLocation}, NT
|
<span className="font-medium">Settled in:</span>{" "}
|
||||||
|
{person.settlementLocation}, NT
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</Link>
|
</Link>
|
||||||
|
</motion.div>
|
||||||
))}
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SearchResults
|
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,43 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
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 { searchMigrants } from "../services/migrantService"
|
import { searchMigrants } from "../services/migrantService";
|
||||||
|
|
||||||
const SearchSection = () => {
|
const SearchSection = () => {
|
||||||
const [searchResults, setSearchResults] = useState<SearchResult[]>([])
|
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
||||||
const [isSearching, setIsSearching] = useState(false)
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [hasSearched, setHasSearched] = useState(false)
|
const [hasSearched, setHasSearched] = useState(false);
|
||||||
|
|
||||||
const handleSearch = async (params: SearchParams) => {
|
const handleSearch = async (params: SearchParams) => {
|
||||||
setIsSearching(true)
|
setIsSearching(true);
|
||||||
try {
|
try {
|
||||||
const results = await searchMigrants(params)
|
const results = await searchMigrants(params);
|
||||||
setSearchResults(results)
|
setSearchResults(results);
|
||||||
setHasSearched(true)
|
setHasSearched(true);
|
||||||
} catch (error) {
|
} 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
|
// In a real application, you would handle this error more gracefully
|
||||||
} finally {
|
} finally {
|
||||||
setIsSearching(false)
|
setIsSearching(false);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="py-16 px-4 md:px-8 bg-gray-50">
|
<section className="py-16 px-4 md:px-8 bg-gray-50">
|
||||||
<div className="max-w-6xl mx-auto">
|
<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} />
|
<SearchForm onSearch={handleSearch} />
|
||||||
{(isSearching || hasSearched) && <SearchResults results={searchResults} isLoading={isSearching} />}
|
{(isSearching || hasSearched) && (
|
||||||
|
<SearchResults results={searchResults} isLoading={isSearching} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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 (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="card-footer"
|
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}
|
{...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 { StrictMode } from "react";
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from "react-dom/client";
|
||||||
import './index.css'
|
import "./index.css";
|
||||||
import App from './App.tsx'
|
import App from "./App.tsx";
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>,
|
</StrictMode>
|
||||||
)
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
import HeroSection from "../components/HeroSection"
|
import HeroSection from "../components/HeroSection";
|
||||||
import IntroSection from "../components/IntroSection"
|
import IntroSection from "../components/IntroSection";
|
||||||
import SearchSection from "../components/SearchSection"
|
import SearchSection from "../components/SearchSection";
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
return (
|
return (
|
||||||
<main>
|
<main>
|
||||||
<HeroSection />
|
<HeroSection />
|
||||||
<IntroSection />
|
|
||||||
<SearchSection />
|
<SearchSection />
|
||||||
|
<IntroSection />
|
||||||
</main>
|
</main>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default HomePage
|
export default HomePage;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue