updated ui

This commit is contained in:
marki1212 2025-05-14 00:15:48 +08:00
parent 73db59a56e
commit a3d3ff99c9
18 changed files with 858 additions and 359 deletions

111
package-lock.json generated
View File

@ -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",

View File

@ -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",

BIN
public/hero.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View File

@ -0,0 +1,2 @@
@import "tailwindcss";
@import "tw-animate-css";

View File

@ -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>

View File

@ -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>
); );
}; };

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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>
); );

View File

@ -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

View File

@ -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;

View File

@ -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 }

View File

@ -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}
/> />
); );

View File

@ -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,
}

View File

@ -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 }

View File

@ -1,10 +1,10 @@
import { StrictMode } from 'react' import { StrictMode } from "react";
import { createRoot } from 'react-dom/client' import { createRoot } from "react-dom/client";
import './index.css' import "./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>
) );

View File

@ -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;