refactor: migrate to protected routes and enhance UI components

This commit is contained in:
mark 2025-05-28 05:02:51 +08:00
parent 5d1c3576cf
commit a340ab6abc
19 changed files with 795 additions and 602 deletions

10
build-local.sh Normal file
View File

@ -0,0 +1,10 @@
#!/bin/bash
set -e
echo "Building application..."
npm run build
echo "Deploying to Laravel public/assets folder..."
cp -r dist/* /home/mark/nun/public/assets
echo "Deployment complete!"

19
build.sh Executable file
View File

@ -0,0 +1,19 @@
#!/bin/bash
set -e
echo "Building React app..."
npm run build
echo "Copying React build files to Laravel public folder..."
# Copy index.html to public root
cp dist/index.html /home/mark/nun/public/index.html
# Copy assets folder contents to public/assets folder
cp -r dist/assets/* /home/mark/nun/public/assets/
echo "Deployment complete!"

View File

@ -1,47 +1,144 @@
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
// import { Toaster } from "react-hot-toast"; // ✅ Import Toaster import { useState, useEffect, type ReactNode } from "react";
import { Toaster } from "react-hot-toast";
import "./App.css";
// import MigrantProfilePage from "./pages/MigrantProfilePage"; // Pages
import NotFoundPage from "./pages/NotFoundPage";
import LoginPage from "./components/admin/LoginPage";
import Migrants from "./components/admin/Migrants";
import ProfileSettings from "./components/ui/ProfileSettings";
import AdminDashboardPage from "./components/admin/AdminDashboard";
import HomePage from "./pages/HomePage"; import HomePage from "./pages/HomePage";
import NotFoundPage from "./pages/NotFoundPage";
import MigrantProfilePage from "./pages/MigrantProfilePage";
// Admin Components
import LoginPage from "./components/admin/LoginPage";
import RegisterPage from "./components/admin/Register"; import RegisterPage from "./components/admin/Register";
import AdminDashboardPage from "./components/admin/AdminDashboard";
import Migrants from "./components/admin/Migrants";
import AddMigrantPage from "./components/admin/AddMigrant"; import AddMigrantPage from "./components/admin/AddMigrant";
import SettingsPage from "./components/admin/Setting";
import ReportsPage from "./components/admin/Reports";
import EditMigrant from "./components/admin/EditMigrant"; import EditMigrant from "./components/admin/EditMigrant";
import SearchResults from "./components/home/SearchResults"; import SettingsPage from "./components/admin/Setting";
import Sample from "./components/admin/Table"; import ProfileSettings from "./components/ui/ProfileSettings";
import ReportsPage from "./components/admin/Reports";
import UserCreate from "./components/admin/users/UserCreate"; import UserCreate from "./components/admin/users/UserCreate";
import { Toaster } from 'react-hot-toast';import "./App.css"; import Sample from "./components/admin/Table";
// Charts
import { MigrationChart } from "./components/charts/MigrationChart"; import { MigrationChart } from "./components/charts/MigrationChart";
import { ResidenceChart } from "./components/charts/ResidenceChart"; import { ResidenceChart } from "./components/charts/ResidenceChart";
import MigrantProfilePage from "./pages/MigrantProfilePage";
// Other
import SearchResults from "./components/home/SearchResults";
import apiService from "./services/apiService";
// Types
interface AuthContextType {
isAuthenticated: boolean;
isLoading: boolean;
user: any;
}
interface ProtectedRouteProps {
children: ReactNode;
}
// Protected Route
function ProtectedRoute({ children }: ProtectedRouteProps) {
const [authState, setAuthState] = useState<AuthContextType>({
isAuthenticated: false,
isLoading: true,
user: null,
});
useEffect(() => {
const checkAuth = async () => {
try {
const token = localStorage.getItem("token");
if (!token) return setAuthState({ isAuthenticated: false, isLoading: false, user: null });
const user = await apiService.fetchCurrentUser();
setAuthState({ isAuthenticated: true, isLoading: false, user });
} catch {
localStorage.removeItem("token");
localStorage.removeItem("user");
setAuthState({ isAuthenticated: false, isLoading: false, user: null });
}
};
checkAuth();
}, []);
if (authState.isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-[#9B2335] mx-auto mb-4"></div>
<p className="text-gray-600">Verifying authentication...</p>
</div>
</div>
);
}
return authState.isAuthenticated ? <>{children}</> : <Navigate to="/login" replace />;
}
// Public Route
function PublicRoute({ children }: ProtectedRouteProps) {
const [isAuthenticated, setIsAuthenticated] = useState<boolean | null>(null);
useEffect(() => {
const checkAuthStatus = async () => {
try {
const token = localStorage.getItem("token");
if (!token) return setIsAuthenticated(false);
await apiService.fetchCurrentUser();
setIsAuthenticated(true);
} catch {
localStorage.removeItem("token");
localStorage.removeItem("user");
setIsAuthenticated(false);
}
};
checkAuthStatus();
}, []);
if (isAuthenticated === null) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#9B2335]"></div>
</div>
);
}
return isAuthenticated ? <Navigate to="/admin" replace /> : <>{children}</>;
}
function App() { function App() {
return ( return (
<Router> <Router>
{/* ✅ Add the Toaster at root level so it works everywhere */} <Toaster position="top-right" reverseOrder={false} />
<Toaster position="top-right" reverseOrder={false} />
<Routes> <Routes>
<Route path="/migrant-profile/:id" element={<MigrantProfilePage />} /> {/* Public Routes */}
<Route path="/" element={<HomePage />} />
<Route path="/migrant-profile/:id" element={<MigrantProfilePage />} />
<Route path="/search-results" element={<SearchResults />} />
<Route path="/migration-chart" element={<MigrationChart />} /> <Route path="/migration-chart" element={<MigrationChart />} />
<Route path="/residence-chart" element={<ResidenceChart />} /> <Route path="/residence-chart" element={<ResidenceChart />} />
<Route path="/search-results" element={<SearchResults />} />
<Route path="/sample" element={<Sample />} /> {/* Auth Routes */}
<Route path="/admin/settings/profile" element={<ProfileSettings />} /> <Route path="/login" element={<PublicRoute><LoginPage /></PublicRoute>} />
<Route path="/admin/migrants" element={<Migrants />} /> <Route path="/register" element={<PublicRoute><RegisterPage /></PublicRoute>} />
<Route path="/admin" element={<AdminDashboardPage />} />
<Route path="/admin/migrants/add" element={<AddMigrantPage />} /> {/* Admin Protected Routes */}
<Route path="/admin/settings" element={<SettingsPage />} /> <Route path="/admin" element={<ProtectedRoute><AdminDashboardPage /></ProtectedRoute>} />
<Route path="/admin/reports" element={<ReportsPage />} /> <Route path="/admin/migrants" element={<ProtectedRoute><Migrants /></ProtectedRoute>} />
<Route path="/admin/migrants/edit/:id" element={<EditMigrant />} /> <Route path="/admin/migrants/add" element={<ProtectedRoute><AddMigrantPage /></ProtectedRoute>} />
<Route path="/admin/users/create" element={<UserCreate />} /> <Route path="/admin/migrants/edit/:id" element={<ProtectedRoute><EditMigrant /></ProtectedRoute>} />
<Route path="/login" element={<LoginPage />} /> <Route path="/admin/settings" element={<ProtectedRoute><SettingsPage /></ProtectedRoute>} />
<Route path="/register" element={<RegisterPage />} /> <Route path="/admin/settings/profile" element={<ProtectedRoute><ProfileSettings /></ProtectedRoute>} />
<Route path="/" element={<HomePage />} /> <Route path="/admin/reports" element={<ProtectedRoute><ReportsPage /></ProtectedRoute>} />
<Route path="/admin/users/create" element={<ProtectedRoute><UserCreate /></ProtectedRoute>} />
<Route path="/sample" element={<ProtectedRoute><Sample /></ProtectedRoute>} />
{/* Catch All */}
<Route path="*" element={<NotFoundPage />} /> <Route path="*" element={<NotFoundPage />} />
</Routes> </Routes>
</Router> </Router>

View File

@ -1,8 +1,6 @@
"use client" "use client"
import type React from "react" import React, { useState } from "react"
import { useState } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { Eye, EyeOff, Lock, Mail } from "lucide-react" import { Eye, EyeOff, Lock, Mail } from "lucide-react"
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
@ -20,22 +18,18 @@ export default function LoginPage() {
const navigate = useNavigate() const navigate = useNavigate()
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault() // Prevent page reload
setError("") setError("")
setIsLoading(true) setIsLoading(true)
try { try {
const response = await apiService.login({ const response = await apiService.login({ email, password })
email, console.log("Response:", response)
password,
})
console.log("Response:", response.data)
alert("Login successful!")
navigate("/admin") navigate("/admin")
} catch (error: any) { } catch (err: any) {
console.error("Error submitting form:", error) console.error("Login error:", err)
if (error.response && error.response.data && error.response.data.message) { if (err.response?.data?.message) {
setError(error.response.data.message) setError(err.response.data.message)
} else { } else {
setError("Login failed. Please check your input and try again.") setError("Login failed. Please check your input and try again.")
} }
@ -65,9 +59,11 @@ export default function LoginPage() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
autoComplete="username"
/> />
</div> </div>
</div> </div>
<div> <div>
<Label htmlFor="password">Password</Label> <Label htmlFor="password">Password</Label>
<div className="relative"> <div className="relative">
@ -79,18 +75,27 @@ export default function LoginPage() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
autoComplete="current-password"
/> />
<button <button
type="button" type="button"
className="absolute right-3 top-2.5 text-gray-500 hover:text-gray-700" className="absolute right-3 top-2.5 text-gray-500 hover:text-gray-700"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
aria-label={showPassword ? "Hide password" : "Show password"}
> >
{showPassword ? <EyeOff className="size-5" /> : <Eye className="size-5" />} {showPassword ? <EyeOff className="size-5" /> : <Eye className="size-5" />}
</button> </button>
</div> </div>
</div> </div>
{error && <p className="text-sm text-red-600">{error}</p>}
{/* Error message */}
{error && (
<div className="rounded-md bg-red-100 px-4 py-2 text-sm text-red-700 border border-red-300 animate-fade-in">
{error}
</div>
)}
</CardContent> </CardContent>
<CardFooter> <CardFooter>
<Button type="submit" className="w-full mt-4" disabled={isLoading}> <Button type="submit" className="w-full mt-4" disabled={isLoading}>
{isLoading ? "Authenticating..." : "Sign In"} {isLoading ? "Authenticating..." : "Sign In"}

View File

@ -2,9 +2,9 @@
import type React from "react" import type React from "react"
import { useState } from "react" import { useState, useEffect } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { UserPlus, ArrowLeft } from "lucide-react" import { UserPlus, ArrowLeft, User as UserIcon } from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
@ -13,16 +13,42 @@ import { toast } from "react-hot-toast"
import apiService from "@/services/apiService" import apiService from "@/services/apiService"
import Header from "@/components/layout/Header" import Header from "@/components/layout/Header"
import Sidebar from "@/components/layout/Sidebar" import Sidebar from "@/components/layout/Sidebar"
import type { User } from "@/types/api"
export default function UserCreate() { export default function UserCreate() {
const navigate = useNavigate() const navigate = useNavigate()
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [formData, setFormData] = useState({ const [user, setUser] = useState<User | null>(null)
name: "", const [formData, setFormData] = useState<User>({
email: "", name: '',
password: "", email: '',
password_confirmation: "", current_password: '',
}) password: '',
password_confirmation: '',
});
useEffect(() => {
async function loadUser() {
try {
const currentUser = await apiService.fetchCurrentUser()
console.log("Setting component - Fetched user:", currentUser)
setUser(currentUser)
// Pre-fill the form with current user data if available
if (currentUser) {
setFormData(prev => ({
...prev,
name: currentUser.name || "",
email: currentUser.email || ""
}))
}
} catch (error) {
console.error("Failed to fetch user info:", error)
toast.error("Failed to load user information")
}
}
loadUser()
}, [])
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target const { name, value } = e.target
@ -32,35 +58,47 @@ export default function UserCreate() {
})) }))
} }
// In your React component:
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault();
// Basic validation if (!formData.name || !formData.email || !formData.current_password) {
if (!formData.name || !formData.email || !formData.password) { toast.error("Please fill in all required fields");
toast.error("Please fill in all required fields") return;
return
} }
if (formData.password !== formData.password_confirmation) { if (formData.password && formData.password !== formData.password_confirmation) {
toast.error("Passwords don't match") toast.error("Passwords don't match");
return return;
} }
try { try {
setIsSubmitting(true) setIsSubmitting(true);
// This would need to be implemented in your apiService // Call updateUser instead of createUser
await apiService.createUser(formData) const response = await apiService.updateUser({
name: formData.name,
email: formData.email,
current_password: formData.current_password,
password: formData.password, // optional
password_confirmation: formData.password_confirmation, // optional
});
toast.success("User created successfully!") toast.success(response.message || "User updated successfully!");
navigate("/admin/settings") // Redirect to an appropriate page navigate("/admin/settings");
} catch (error) { } catch (error: any) {
console.error("Error creating user:", error) console.error("Error updating user:", error);
toast.error("Failed to create user. Please try again.")
if (error.response?.data?.message) {
toast.error(error.response.data.message);
} else {
toast.error("Failed to update user. Please try again.");
}
} finally { } finally {
setIsSubmitting(false) setIsSubmitting(false);
} }
} };
return ( return (
<div className="flex min-h-dvh bg-gray-950"> <div className="flex min-h-dvh bg-gray-950">
@ -70,10 +108,10 @@ export default function UserCreate() {
<main className="p-4 md:p-6"> <main className="p-4 md:p-6">
<div className="mb-6"> <div className="mb-6">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={() => navigate("/admin/dashboard")} onClick={() => navigate("/admin/dashboard")}
className="hover:bg-gray-800 text-gray-400 hover:text-white"> className="hover:bg-gray-800 text-gray-400 hover:text-white">
<ArrowLeft className="size-5" /> <ArrowLeft className="size-5" />
</Button> </Button>
@ -82,6 +120,40 @@ export default function UserCreate() {
<p className="text-gray-400 mt-2 ml-10">Manage your profile and security preferences</p> <p className="text-gray-400 mt-2 ml-10">Manage your profile and security preferences</p>
</div> </div>
{/* Current user information card */}
{user && (
<div className="mb-6 max-w-10xl mx-auto">
<Card className="shadow-xl border border-gray-800 bg-gray-900 overflow-hidden mb-6">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-[#9B2335] to-[#9B2335]/60"></div>
<CardHeader className="border-b border-gray-800">
<CardTitle className="text-xl font-serif text-white flex items-center">
<UserIcon className="mr-2 size-5" />
Current User Profile
</CardTitle>
</CardHeader>
<CardContent className="p-6">
<div className="flex flex-col md:flex-row gap-6">
<div className="flex-shrink-0 flex justify-center">
<div className="size-24 rounded-full bg-[#9B2335] flex items-center justify-center border-4 border-gray-800">
<span className="text-3xl font-bold text-white">{user.name.charAt(0).toUpperCase()}</span>
</div>
</div>
<div className="flex-grow space-y-4">
<div>
<h3 className="text-gray-400 text-sm">Full Name</h3>
<p className="text-white text-lg font-medium">{user.name}</p>
</div>
<div>
<h3 className="text-gray-400 text-sm">Email Address</h3>
<p className="text-white text-lg font-medium">{user.email}</p>
</div>
</div>
</div>
</CardContent>
</Card>
</div>
)}
<div className="max-w-10xl mx-auto"> <div className="max-w-10xl mx-auto">
<Card className="shadow-2xl border border-gray-800 bg-gray-900 overflow-hidden"> <Card className="shadow-2xl border border-gray-800 bg-gray-900 overflow-hidden">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-[#9B2335] to-[#9B2335]/60"></div> <div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-[#9B2335] to-[#9B2335]/60"></div>
@ -135,11 +207,14 @@ export default function UserCreate() {
name="current_password" name="current_password"
type="password" type="password"
placeholder="Enter current password" placeholder="Enter current password"
value={formData.current_password}
onChange={handleInputChange}
required required
className="bg-gray-800 border-gray-700 text-white focus:border-[#9B2335] placeholder:text-gray-500" className="bg-gray-800 border-gray-700 text-white focus:border-[#9B2335] placeholder:text-gray-500"
/> />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="password" className="text-gray-300"> <Label htmlFor="password" className="text-gray-300">

View File

@ -11,11 +11,11 @@ import { Progress } from "@/components/ui/progress"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Label } from "@/components/ui/label" import { Label } from "@/components/ui/label"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import type { Person, Migration, Naturalization, Residence, Family, Internment, ExistingPhoto } from "@/types/api"
import { import {
ChevronLeft, ChevronLeft,
ChevronRight, ChevronRight,
Save, Save,
Edit3,
User, User,
MapPin, MapPin,
FileText, FileText,
@ -25,7 +25,6 @@ import {
Camera, Camera,
} from "lucide-react" } from "lucide-react"
import apiService from "@/services/apiService" import apiService from "@/services/apiService"
import { useTabsPaneFormSubmit } from "@/hooks/useTabsPaneFormSubmit"
import { showSuccessToast, showErrorToast, showUpdateItemToast } from "@/utils/toast" import { showSuccessToast, showErrorToast, showUpdateItemToast } from "@/utils/toast"
// Import confirmation modals // Import confirmation modals
@ -57,59 +56,6 @@ const stepDescriptions = [
const stepIcons = [User, MapPin, FileText, Home, Users, Shield, Camera] const stepIcons = [User, MapPin, FileText, Home, Users, Shield, Camera]
// Type definitions for better type safety
interface PersonData {
surname: string
christian_name: string
date_of_birth: string
place_of_birth: string
date_of_death: string
occupation: string
additional_notes: string
reference: string
id_card_no: string
}
interface MigrationData {
date_of_arrival_aus: string
date_of_arrival_nt: string
arrival_period: string
data_source: string
}
interface NaturalizationData {
date_of_naturalisation: string
no_of_cert: string
issued_at: string
}
interface ResidenceData {
town_or_city: string
home_at_death: string
}
interface FamilyData {
names_of_parents: string
names_of_children: string
}
interface InternmentData {
corps_issued: string
interned_in: string
sent_to: string
internee_occupation: string
internee_address: string
cav: string
}
interface ExistingPhoto {
id: number
caption?: string
is_profile_photo?: boolean
filename?: string
url?: string
}
const StepperForm = () => { const StepperForm = () => {
const { id } = useParams<{ id: string }>() const { id } = useParams<{ id: string }>()
const navigate = useNavigate() const navigate = useNavigate()
@ -126,7 +72,7 @@ const StepperForm = () => {
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
// Initial state functions // Initial state functions
const getInitialPersonState = (): PersonData => ({ const getInitialPersonState = (): Person => ({
surname: "", surname: "",
christian_name: "", christian_name: "",
date_of_birth: "", date_of_birth: "",
@ -136,32 +82,33 @@ const StepperForm = () => {
additional_notes: "", additional_notes: "",
reference: "", reference: "",
id_card_no: "", id_card_no: "",
person_id: 0,
}) })
const getInitialMigrationState = (): MigrationData => ({ const getInitialMigrationState = (): Migration => ({
date_of_arrival_aus: "", date_of_arrival_aus: "",
date_of_arrival_nt: "", date_of_arrival_nt: "",
arrival_period: "", arrival_period: "",
data_source: "", data_source: "",
}) })
const getInitialNaturalizationState = (): NaturalizationData => ({ const getInitialNaturalizationState = (): Naturalization => ({
date_of_naturalisation: "", date_of_naturalisation: "",
no_of_cert: "", no_of_cert: "",
issued_at: "", issued_at: "",
}) })
const getInitialResidenceState = (): ResidenceData => ({ const getInitialResidenceState = (): Residence => ({
town_or_city: "", town_or_city: "",
home_at_death: "", home_at_death: "",
}) })
const getInitialFamilyState = (): FamilyData => ({ const getInitialFamilyState = (): Family => ({
names_of_parents: "", names_of_parents: "",
names_of_children: "", names_of_children: "",
}) })
const getInitialInternmentState = (): InternmentData => ({ const getInitialInternmentState = (): Internment => ({
corps_issued: "", corps_issued: "",
interned_in: "", interned_in: "",
sent_to: "", sent_to: "",
@ -171,12 +118,12 @@ const StepperForm = () => {
}) })
// Form data state // Form data state
const [person, setPerson] = useState<PersonData>(getInitialPersonState()) const [person, setPerson] = useState<Person>(getInitialPersonState())
const [migration, setMigration] = useState<MigrationData>(getInitialMigrationState()) const [migration, setMigration] = useState<Migration>(getInitialMigrationState())
const [naturalization, setNaturalization] = useState<NaturalizationData>(getInitialNaturalizationState()) const [naturalization, setNaturalization] = useState<Naturalization>(getInitialNaturalizationState())
const [residence, setResidence] = useState<ResidenceData>(getInitialResidenceState()) const [residence, setResidence] = useState<Residence>(getInitialResidenceState())
const [family, setFamily] = useState<FamilyData>(getInitialFamilyState()) const [family, setFamily] = useState<Family>(getInitialFamilyState())
const [internment, setInternment] = useState<InternmentData>(getInitialInternmentState()) const [internment, setInternment] = useState<Internment>(getInitialInternmentState())
// Photo state // Photo state
const [photos, setPhotos] = useState<File[]>([]) const [photos, setPhotos] = useState<File[]>([])
@ -207,15 +154,15 @@ const StepperForm = () => {
const loadExistingData = async () => { const loadExistingData = async () => {
try { try {
setLoading(true) setLoading(true)
const personId = Number.parseInt(id!, 10) const personId = Number.parseInt(id!, 10)
if (isNaN(personId)) { if (isNaN(personId)) {
throw new Error("Invalid person ID") throw new Error("Invalid person ID")
} }
const migrantData = await apiService.getMigrantById(personId) const migrantData = await apiService.getMigrantById(personId)
// Populate person data // FIX: Populate person data from migrantData.person, not current person state
setPerson({ setPerson({
surname: migrantData.surname || "", surname: migrantData.surname || "",
christian_name: migrantData.christian_name || "", christian_name: migrantData.christian_name || "",
@ -226,8 +173,9 @@ const StepperForm = () => {
additional_notes: migrantData.additional_notes || "", additional_notes: migrantData.additional_notes || "",
reference: migrantData.reference || "", reference: migrantData.reference || "",
id_card_no: migrantData.id_card_no || "", id_card_no: migrantData.id_card_no || "",
person_id: migrantData.person_id || 0,
}) })
// Populate migration data // Populate migration data
if (migrantData.migration) { if (migrantData.migration) {
setMigration({ setMigration({
@ -237,7 +185,7 @@ const StepperForm = () => {
data_source: migrantData.migration.data_source || "", data_source: migrantData.migration.data_source || "",
}) })
} }
// Populate naturalization data // Populate naturalization data
if (migrantData.naturalization) { if (migrantData.naturalization) {
setNaturalization({ setNaturalization({
@ -246,7 +194,7 @@ const StepperForm = () => {
issued_at: migrantData.naturalization.issued_at || "", issued_at: migrantData.naturalization.issued_at || "",
}) })
} }
// Populate residence data // Populate residence data
if (migrantData.residence) { if (migrantData.residence) {
setResidence({ setResidence({
@ -254,7 +202,7 @@ const StepperForm = () => {
home_at_death: migrantData.residence.home_at_death || "", home_at_death: migrantData.residence.home_at_death || "",
}) })
} }
// Populate family data // Populate family data
if (migrantData.family) { if (migrantData.family) {
setFamily({ setFamily({
@ -262,7 +210,7 @@ const StepperForm = () => {
names_of_children: migrantData.family.names_of_children || "", names_of_children: migrantData.family.names_of_children || "",
}) })
} }
// Populate internment data // Populate internment data
if (migrantData.internment) { if (migrantData.internment) {
setInternment({ setInternment({
@ -274,8 +222,7 @@ const StepperForm = () => {
cav: migrantData.internment.cav || "", cav: migrantData.internment.cav || "",
}) })
} }
// Handle existing photos with better logic
if (migrantData.photos && Array.isArray(migrantData.photos) && migrantData.photos.length > 0) { if (migrantData.photos && Array.isArray(migrantData.photos) && migrantData.photos.length > 0) {
const photoData = migrantData.photos as ExistingPhoto[] const photoData = migrantData.photos as ExistingPhoto[]
setExistingPhotos(photoData) setExistingPhotos(photoData)
@ -290,7 +237,7 @@ const StepperForm = () => {
setMainPhotoIndex(mainPhotoIdx) setMainPhotoIndex(mainPhotoIdx)
} }
} }
setInitialDataLoaded(true) setInitialDataLoaded(true)
} catch (error) { } catch (error) {
console.error("Error loading migrant data:", error) console.error("Error loading migrant data:", error)
@ -319,24 +266,19 @@ const StepperForm = () => {
if (e.target.files && e.target.files.length > 0) { if (e.target.files && e.target.files.length > 0) {
const selectedFiles = Array.from(e.target.files) const selectedFiles = Array.from(e.target.files)
// Add new photos
setPhotos((prev) => [...prev, ...selectedFiles]) setPhotos((prev) => [...prev, ...selectedFiles])
// Create previews for new photos
const newPreviews = selectedFiles.map((file) => URL.createObjectURL(file)) const newPreviews = selectedFiles.map((file) => URL.createObjectURL(file))
setPhotoPreviews((prev) => [...prev, ...newPreviews]) setPhotoPreviews((prev) => [...prev, ...newPreviews])
// Add captions for new photos
setCaptions((prev) => { setCaptions((prev) => {
const newCaptions = [...prev] const newCaptions = [...prev]
// Add empty captions for new photos
for (let i = 0; i < selectedFiles.length; i++) { for (let i = 0; i < selectedFiles.length; i++) {
newCaptions.push("") newCaptions.push("")
} }
return newCaptions return newCaptions
}) })
// Set main photo index if none exists and we have photos
if (mainPhotoIndex === null && (existingPhotos.length > 0 || selectedFiles.length > 0)) { if (mainPhotoIndex === null && (existingPhotos.length > 0 || selectedFiles.length > 0)) {
setMainPhotoIndex(0) setMainPhotoIndex(0)
} }
@ -349,7 +291,6 @@ const StepperForm = () => {
const removeExistingPhoto = (index: number) => { const removeExistingPhoto = (index: number) => {
const wasMainPhoto = mainPhotoIndex === index const wasMainPhoto = mainPhotoIndex === index
// Remove the photo from existing photos
setExistingPhotos((prev) => prev.filter((_, i) => i !== index)) setExistingPhotos((prev) => prev.filter((_, i) => i !== index))
// Remove corresponding caption // Remove corresponding caption
@ -408,17 +349,14 @@ const StepperForm = () => {
const submitForm = async () => { const submitForm = async () => {
try { try {
const formData = new FormData() const formData = new FormData()
// Add person data // Add person data
console.log('Person data:', person); console.log('Person data:', person);
Object.entries(person).forEach(([key, value]) => { Object.entries(person).forEach(([key, value]) => {
if (value) formData.append(key, value) if (value) formData.append(key, value)
}) })
// Add migration data // Add migration data
console.log('Migration data:', migration); console.log('Migration data:', migration);
if (Object.values(migration).some(v => v)) { if (Object.values(migration).some(v => v)) {
@ -426,14 +364,14 @@ const StepperForm = () => {
if (value) formData.append(`migration[${key}]`, value) if (value) formData.append(`migration[${key}]`, value)
}) })
} }
// Add naturalization data // Add naturalization data
if (Object.values(naturalization).some(v => v)) { if (Object.values(naturalization).some(v => v)) {
Object.entries(naturalization).forEach(([key, value]) => { Object.entries(naturalization).forEach(([key, value]) => {
if (value) formData.append(`naturalization[${key}]`, value) if (value) formData.append(`naturalization[${key}]`, value)
}) })
} }
// Add residence data // Add residence data
console.log('Residence data:', residence); console.log('Residence data:', residence);
if (Object.values(residence).some(v => v)) { if (Object.values(residence).some(v => v)) {
@ -441,7 +379,7 @@ const StepperForm = () => {
if (value) formData.append(`residence[${key}]`, value) if (value) formData.append(`residence[${key}]`, value)
}) })
} }
// Add family data // Add family data
console.log('Family data:', family); console.log('Family data:', family);
if (Object.values(family).some(v => v)) { if (Object.values(family).some(v => v)) {
@ -449,7 +387,7 @@ const StepperForm = () => {
if (value) formData.append(`family[${key}]`, value) if (value) formData.append(`family[${key}]`, value)
}) })
} }
// Add internment data // Add internment data
console.log('Internment data:', internment); console.log('Internment data:', internment);
if (Object.values(internment).some(v => v)) { if (Object.values(internment).some(v => v)) {
@ -457,48 +395,101 @@ const StepperForm = () => {
if (value) formData.append(`internment[${key}]`, value) if (value) formData.append(`internment[${key}]`, value)
}) })
} }
// Add new photos // Add photos
console.log('Photos:', photos); console.log('Photos:', photos);
photos.forEach((photo) => { photos.forEach((photo) => {
formData.append('photos[]', photo) formData.append('photos[]', photo)
}) })
// Add captions // FIXED: Add captions for new photos only (existing photo captions handled separately)
console.log('Captions:', captions); console.log('New photo captions:');
captions.forEach((caption, index) => { const newPhotoCaptions = captions.slice(existingPhotos.length); // Get captions for new photos only
if (caption) { newPhotoCaptions.forEach((caption, index) => {
formData.append(`captions[${index}]`, caption) console.log(`New photo ${index} caption:`, caption);
} formData.append(`captions[${index}]`, caption || '') // Always send caption, even if empty
}) })
// Handle main photo logic // IMPROVED MAIN PHOTO LOGIC
console.log('=== MAIN PHOTO DEBUG ===');
console.log('Main photo index:', mainPhotoIndex); console.log('Main photo index:', mainPhotoIndex);
console.log('Existing photos count:', existingPhotos.length);
console.log('New photos count:', photos.length);
console.log('Is edit mode:', isEditMode);
// Always send profile photo data if mainPhotoIndex is set
if (mainPhotoIndex !== null) { if (mainPhotoIndex !== null) {
formData.append('set_as_profile', 'true') formData.append('set_as_profile', 'true')
console.log('Existing photos:', existingPhotos);
if (mainPhotoIndex < existingPhotos.length) { if (mainPhotoIndex < existingPhotos.length) {
// Main photo is an existing photo // Main photo is an existing photo
const existingPhoto = existingPhotos[mainPhotoIndex] const existingPhoto = existingPhotos[mainPhotoIndex]
console.log('Setting existing photo as main:', existingPhoto.id);
formData.append('profile_photo_id', existingPhoto.id.toString()) formData.append('profile_photo_id', existingPhoto.id.toString())
} else { } else {
// Main photo is a new photo // Main photo is a new photo
const newPhotoIndex = mainPhotoIndex - existingPhotos.length const newPhotoIndex = mainPhotoIndex - existingPhotos.length
console.log('New photo index:', newPhotoIndex); console.log('Setting new photo as main, index:', newPhotoIndex);
formData.append('profile_photo_index', newPhotoIndex.toString())
// Validate the index
if (newPhotoIndex >= 0 && newPhotoIndex < photos.length) {
formData.append('profile_photo_index', newPhotoIndex.toString())
console.log('✅ Valid new photo index sent:', newPhotoIndex);
// CRITICAL FIX: Ensure the photo at this index is marked as profile
formData.append('new_profile_photo_index', newPhotoIndex.toString())
} else {
console.error('❌ Invalid new photo index:', newPhotoIndex, 'Photos length:', photos.length);
}
}
} else {
console.log('⚠️ No main photo selected');
formData.append('set_as_profile', 'false')
}
// Handle existing photo updates (captions and profile status)
if (existingPhotos.length > 0) {
console.log('Processing existing photos for caption updates...');
existingPhotos.forEach((photo, index) => {
formData.append(`existing_photos[${index}][id]`, photo.id.toString())
// Send caption (from the first part of captions array)
const caption = captions[index] || ''
formData.append(`existing_photos[${index}][caption]`, caption)
console.log(`Caption for existing photo ${photo.id}:`, caption);
// FIXED: Explicitly set profile status for existing photos
const isThisPhotoMain = mainPhotoIndex === index
formData.append(`existing_photos[${index}][is_profile_photo]`, isThisPhotoMain ? 'true' : 'false')
console.log(`Existing photo ${photo.id} is_profile_photo:`, isThisPhotoMain);
})
}
// ADDITIONAL FIX: Send explicit photo metadata for new photos
photos.forEach((_photo, index) => {
const totalIndex = existingPhotos.length + index
const isThisPhotoMain = mainPhotoIndex === totalIndex
formData.append(`new_photos[${index}][is_profile_photo]`, isThisPhotoMain ? 'true' : 'false')
formData.append(`new_photos[${index}][caption]`, captions[totalIndex] || '')
console.log(`New photo ${index} metadata:`, {
is_profile_photo: isThisPhotoMain,
caption: captions[totalIndex] || '',
total_index: totalIndex
});
})
// Log FormData contents for debugging
console.log('=== FORM DATA CONTENTS ===');
for (let [key, value] of formData.entries()) {
if (value instanceof File) {
console.log(key, ':', `[File: ${value.name}]`);
} else {
console.log(key, ':', value);
} }
} }
// Handle existing photo updates (captions)
console.log('Existing photos:', existingPhotos);
existingPhotos.forEach((photo, index) => {
formData.append(`existing_photos[${index}][id]`, photo.id.toString())
if (captions[index]) {
formData.append(`existing_photos[${index}][caption]`, captions[index])
}
})
let response let response
if (isEditMode && id) { if (isEditMode && id) {
console.log('Updating migrant:', id); console.log('Updating migrant:', id);
@ -507,7 +498,7 @@ const StepperForm = () => {
console.log('Creating new migrant'); console.log('Creating new migrant');
response = await apiService.createMigrant(formData) response = await apiService.createMigrant(formData)
} }
return response return response
} catch (error) { } catch (error) {
console.error('Form submission error:', error) console.error('Form submission error:', error)

View File

@ -65,12 +65,11 @@ const PhotosStep: React.FC<PhotosStepProps> = ({
</div> </div>
</div> </div>
{/* Existing Photos */}
{existingPhotos.length > 0 && ( {existingPhotos.length > 0 && (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ImageIcon className="w-5 h-5 text-gray-600" /> <ImageIcon className="w-5 h-5 text-gray-600" />
<h3 className="text-lg font-medium text-gray-900">Existing Photos</h3> <h3 className="text-lg font-medium text-white">Existing Photos</h3>
<Badge variant="secondary">{existingPhotos.length}</Badge> <Badge variant="secondary">{existingPhotos.length}</Badge>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
@ -131,12 +130,11 @@ const PhotosStep: React.FC<PhotosStepProps> = ({
</div> </div>
)} )}
{/* New Photos */}
{photos.length > 0 && ( {photos.length > 0 && (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Upload className="w-5 h-5 text-gray-600" /> <Upload className="w-5 h-5 text-gray-600" />
<h3 className="text-lg font-medium text-gray-900">New Photos</h3> <h3 className="text-lg font-medium text-white">New Photos</h3>
<Badge variant="secondary">{photos.length}</Badge> <Badge variant="secondary">{photos.length}</Badge>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">

View File

@ -1,34 +1,46 @@
"use client" "use client"
import type React from "react"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { Search } from "lucide-react" import {
Search,
Mail,
Phone,
MapPin,
Facebook,
Twitter,
Instagram,
Youtube,
Clock,
Users,
Database,
Award,
} from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel" import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel"
import { Card, CardContent } from "@/components/ui/card" import { Card, CardContent } from "@/components/ui/card"
import SearchForm from "./SearchForm" import SearchForm from "./SearchForm"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import ApiService from "@/services/apiService"; import ApiService from "@/services/apiService"
import type { Person } from "@/types/api"; import type { Person } from "@/types/api"
export default function Home() { export default function Home() {
const [currentSlide, setCurrentSlide] = useState(0) const [currentSlide, setCurrentSlide] = useState(0)
const navigate = useNavigate(); const navigate = useNavigate()
const [total, setTotal] = useState(0); const [total, setTotal] = useState(0)
const [migrants, setMigrants] = useState<Person[]>([]); const [migrants, setMigrants] = useState<Person[]>([])
const API_BASE_URL = "http://localhost:8000"; const API_BASE_URL = "http://localhost:8000"
useEffect(() => { useEffect(() => {
async function fetchData() { async function fetchData() {
const response = await ApiService.getMigrants(1, 10); const response = await ApiService.getMigrants(1, 10)
setMigrants(response.data); setMigrants(response.data)
setTotal(response.total); setTotal(response.total)
} }
fetchData(); fetchData()
}, []); }, [])
const backgroundImages = [ const backgroundImages = [
{ {
@ -49,7 +61,6 @@ export default function Home() {
}, },
] ]
const galleryImages = [ const galleryImages = [
{ {
src: "https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=800&q=80", src: "https://images.unsplash.com/photo-1523906834658-6e24ef2386f9?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=800&q=80",
@ -80,6 +91,18 @@ export default function Home() {
return () => clearInterval(timer) return () => clearInterval(timer)
}, [backgroundImages.length]) }, [backgroundImages.length])
// Smooth scroll to contact section
const scrollToContact = (e: React.MouseEvent) => {
e.preventDefault()
const contactSection = document.getElementById("contact")
if (contactSection) {
contactSection.scrollIntoView({
behavior: "smooth",
block: "start",
})
}
}
return ( return (
<div className="flex flex-col min-h-screen"> <div className="flex flex-col min-h-screen">
<header className="absolute top-0 left-0 right-0 z-50 border-b border-white/20 bg-black/20 backdrop-blur-sm"> <header className="absolute top-0 left-0 right-0 z-50 border-b border-white/20 bg-black/20 backdrop-blur-sm">
@ -97,9 +120,13 @@ export default function Home() {
<a href="#stories" className="text-sm font-medium text-white hover:text-white/80 transition-colors"> <a href="#stories" className="text-sm font-medium text-white hover:text-white/80 transition-colors">
Stories Stories
</a> </a>
<Link to="/contact" className="text-sm font-medium text-white hover:text-white/80 transition-colors"> <a
href="#contact"
onClick={scrollToContact}
className="text-sm font-medium text-white hover:text-white/80 transition-colors"
>
Contact Contact
</Link> </a>
</nav> </nav>
<Button variant="outline" size="icon" className="md:hidden border-white/20 text-white hover:bg-white/10"> <Button variant="outline" size="icon" className="md:hidden border-white/20 text-white hover:bg-white/10">
<Search className="h-4 w-4" /> <Search className="h-4 w-4" />
@ -116,8 +143,9 @@ export default function Home() {
{backgroundImages.map((image, index) => ( {backgroundImages.map((image, index) => (
<div <div
key={index} key={index}
className={`absolute inset-0 transition-opacity duration-1000 ${index === currentSlide ? "opacity-100" : "opacity-0" className={`absolute inset-0 transition-opacity duration-1000 ${
}`} index === currentSlide ? "opacity-100" : "opacity-0"
}`}
> >
<img src={image.src || "/placeholder.svg"} alt={image.alt} className="w-full h-full object-cover" /> <img src={image.src || "/placeholder.svg"} alt={image.alt} className="w-full h-full object-cover" />
<div className="absolute inset-0 bg-black/60" /> <div className="absolute inset-0 bg-black/60" />
@ -141,7 +169,6 @@ export default function Home() {
<SearchForm /> <SearchForm />
</div> </div>
<Button <Button
size="lg" size="lg"
variant="outline" variant="outline"
@ -240,7 +267,7 @@ export default function Home() {
<CarouselContent> <CarouselContent>
{migrants.map((person) => { {migrants.map((person) => {
// Find the profile photo // Find the profile photo
const profilePhoto = person.photos?.find((photo) => photo.is_profile_photo); const profilePhoto = person.photos?.find((photo) => photo.is_profile_photo)
return ( return (
<CarouselItem key={person.person_id} className="md:basis-1/2 lg:basis-1/2"> <CarouselItem key={person.person_id} className="md:basis-1/2 lg:basis-1/2">
@ -286,7 +313,7 @@ export default function Home() {
</Card> </Card>
</div> </div>
</CarouselItem> </CarouselItem>
); )
})} })}
</CarouselContent> </CarouselContent>
<CarouselPrevious /> <CarouselPrevious />
@ -343,20 +370,211 @@ export default function Home() {
</div> </div>
</section> </section>
</main> </main>
<footer className="border-t bg-[#1A2A57] text-white">
<div className="container flex flex-col gap-2 sm:flex-row py-6 w-full items-center px-4 md:px-6"> {/* Enhanced Footer with Contact Information */}
<p className="text-xs">&copy; {new Date().getFullYear()} Italian Migrants NT. All rights reserved.</p> <footer id="contact" className="bg-[#1A2A57] text-white">
<nav className="sm:ml-auto flex gap-4 sm:gap-6"> <div className="container px-4 md:px-6">
<Link to="/terms" className="text-xs hover:underline underline-offset-4"> {/* Main Footer Content */}
Terms of Service <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 py-12">
</Link> {/* About Section */}
<Link to="/privacy" className="text-xs hover:underline underline-offset-4"> <div className="space-y-4">
Privacy <h3 className="text-lg font-bold font-serif text-[#E8DCCA]">Italian Migrants NT</h3>
</Link> <p className="text-sm text-gray-300 leading-relaxed">
<Link to="/admin" className="text-xs hover:underline underline-offset-4"> Preserving and celebrating the rich heritage of Italian migrants to the Northern Territory through
Admin digital archives, personal stories, and historical documentation.
</Link> </p>
</nav> <div className="flex items-center gap-2 text-sm text-gray-300">
<Award className="h-4 w-4 text-[#01796F]" />
<span>Heritage Australia Recognition</span>
</div>
</div>
{/* Contact Information */}
<div className="space-y-4">
<h3 className="text-lg font-bold font-serif text-[#E8DCCA]">Contact Us</h3>
<div className="space-y-3">
<div className="flex items-start gap-3">
<MapPin className="h-4 w-4 text-[#01796F] mt-0.5 flex-shrink-0" />
<div className="text-sm text-gray-300">
<p>Northern Territory Archives Centre</p>
<p>Kelsey Crescent, Millner NT 0810</p>
<p>Australia</p>
</div>
</div>
<div className="flex items-center gap-3">
<Phone className="h-4 w-4 text-[#01796F]" />
<span className="text-sm text-gray-300">+61 8 8924 7677</span>
</div>
<div className="flex items-center gap-3">
<Mail className="h-4 w-4 text-[#01796F]" />
<a
href="mailto:heritage@italianmigrantsnt.org.au"
className="text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors"
>
heritage@italianmigrantsnt.org.au
</a>
</div>
<div className="flex items-center gap-3">
<Clock className="h-4 w-4 text-[#01796F]" />
<div className="text-sm text-gray-300">
<p>Mon-Fri: 9:00 AM - 5:00 PM</p>
<p>Sat: 10:00 AM - 2:00 PM</p>
</div>
</div>
</div>
</div>
{/* Quick Links */}
<div className="space-y-4">
<h3 className="text-lg font-bold font-serif text-[#E8DCCA]">Quick Links</h3>
<nav className="space-y-2">
<Link
to="/search-results"
className="block text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors"
>
Search Database
</Link>
<Link to="/contribute" className="block text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors">
Contribute a Story
</Link>
<Link to="/gallery" className="block text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors">
Photo Gallery
</Link>
<Link
to="/research-help"
className="block text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors"
>
Research Help
</Link>
<Link to="/volunteer" className="block text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors">
Volunteer
</Link>
<Link to="/donations" className="block text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors">
Support Us
</Link>
</nav>
</div>
{/* Resources & Social */}
<div className="space-y-4">
<h3 className="text-lg font-bold font-serif text-[#E8DCCA]">Resources</h3>
<nav className="space-y-2">
<Link to="/faq" className="block text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors">
FAQ
</Link>
<Link
to="/research-guides"
className="block text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors"
>
Research Guides
</Link>
<Link
to="/historical-timeline"
className="block text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors"
>
Historical Timeline
</Link>
<Link to="/partnerships" className="block text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors">
Partnerships
</Link>
</nav>
<div className="pt-4">
<h4 className="text-sm font-semibold text-[#E8DCCA] mb-3">Follow Us</h4>
<div className="flex gap-3">
<a
href="https://facebook.com/italianmigrantsnt"
className="text-gray-300 hover:text-[#E8DCCA] transition-colors"
aria-label="Follow us on Facebook"
>
<Facebook className="h-5 w-5" />
</a>
<a
href="https://twitter.com/italianmigrantsnt"
className="text-gray-300 hover:text-[#E8DCCA] transition-colors"
aria-label="Follow us on Twitter"
>
<Twitter className="h-5 w-5" />
</a>
<a
href="https://instagram.com/italianmigrantsnt"
className="text-gray-300 hover:text-[#E8DCCA] transition-colors"
aria-label="Follow us on Instagram"
>
<Instagram className="h-5 w-5" />
</a>
<a
href="https://youtube.com/italianmigrantsnt"
className="text-gray-300 hover:text-[#E8DCCA] transition-colors"
aria-label="Subscribe to our YouTube channel"
>
<Youtube className="h-5 w-5" />
</a>
</div>
</div>
</div>
</div>
{/* Database Stats */}
<div className="border-t border-gray-600 py-8">
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 text-center">
<div className="space-y-1">
<div className="flex items-center justify-center gap-2">
<Database className="h-5 w-5 text-[#01796F]" />
<span className="text-2xl font-bold text-[#E8DCCA]">{total.toLocaleString()}</span>
</div>
<p className="text-sm text-gray-300">Total Records</p>
</div>
<div className="space-y-1">
<div className="flex items-center justify-center gap-2">
<Users className="h-5 w-5 text-[#01796F]" />
<span className="text-2xl font-bold text-[#E8DCCA]">2,847</span>
</div>
<p className="text-sm text-gray-300">Families Documented</p>
</div>
<div className="space-y-1">
<div className="flex items-center justify-center gap-2">
<MapPin className="h-5 w-5 text-[#01796F]" />
<span className="text-2xl font-bold text-[#E8DCCA]">156</span>
</div>
<p className="text-sm text-gray-300">Italian Regions</p>
</div>
<div className="space-y-1">
<div className="flex items-center justify-center gap-2">
<Clock className="h-5 w-5 text-[#01796F]" />
<span className="text-2xl font-bold text-[#E8DCCA]">100+</span>
</div>
<p className="text-sm text-gray-300">Years of History</p>
</div>
</div>
</div>
{/* Bottom Footer */}
<div className="border-t border-gray-600 py-6">
<div className="flex flex-col md:flex-row justify-between items-center gap-4">
<div className="flex flex-col md:flex-row items-center gap-4 text-sm text-gray-300">
<p>&copy; {new Date().getFullYear()} Italian Migrants NT. All rights reserved.</p>
<div className="flex items-center gap-1">
<span>Powered by</span>
<span className="text-[#E8DCCA] font-semibold">Heritage Digital Archives</span>
</div>
</div>
<nav className="flex gap-6">
<Link to="/terms" className="text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors">
Terms of Service
</Link>
<Link to="/privacy" className="text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors">
Privacy Policy
</Link>
<Link to="/accessibility" className="text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors">
Accessibility
</Link>
<Link to="/admin" className="text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors">
Admin
</Link>
</nav>
</div>
</div>
</div> </div>
</footer> </footer>
</div> </div>

View File

@ -47,21 +47,15 @@ export default function MigrantProfile() {
<span className="text-xl font-bold text-[#9B2335]">Italian Migrants NT</span> <span className="text-xl font-bold text-[#9B2335]">Italian Migrants NT</span>
</Link> </Link>
<nav className="hidden md:flex gap-6"> <nav className="hidden md:flex gap-6">
<Link to="/" className="text-sm font-medium hover:underline underline-offset-4"> {["home", "about", "search", "stories", "contact"].map((path) => (
Home <Link
</Link> key={path}
<Link to="/about" className="text-sm font-medium hover:underline underline-offset-4"> to={`/`}
About className="text-sm font-medium hover:underline underline-offset-4 capitalize"
</Link> >
<Link to="/search" className="text-sm font-medium hover:underline underline-offset-4"> {path}
Search </Link>
</Link> ))}
<Link to="/stories" className="text-sm font-medium hover:underline underline-offset-4">
Stories
</Link>
<Link to="/contact" className="text-sm font-medium hover:underline underline-offset-4">
Contact
</Link>
</nav> </nav>
</div> </div>
</header> </header>

View File

@ -1,132 +0,0 @@
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Card, CardContent } from "@/components/ui/card";
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
import { ChevronLeft, ChevronRight, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import AnimatedImage from "@/components/ui/animated-image";
import type { Photo } from "@/types/migrant";
interface PhotoGalleryProps {
photos: Photo[];
}
export default function PhotoGallery({ photos }: PhotoGalleryProps) {
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const handlePrevious = () => {
setCurrentPhotoIndex((prev) => (prev === 0 ? photos.length - 1 : prev - 1));
};
const handleNext = () => {
setCurrentPhotoIndex((prev) => (prev === photos.length - 1 ? 0 : prev + 1));
};
return (
<Card className="overflow-hidden border border-gray-200 shadow-none rounded-md bg-white">
<CardContent className="">
<h2 className="text-2xl font-bold mb-4 font-serif text-gray-800">
Galleria Fotografica
</h2>
<div className="h-1 w-20 bg-gradient-to-r from-green-600 via-white to-red-600 mb-6" />
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{photos.map((photo, index) => (
<Dialog
key={index}
open={isDialogOpen && currentPhotoIndex === index}
onOpenChange={setIsDialogOpen}
>
<DialogTrigger asChild>
<motion.button
className="relative h-40 w-full rounded-md overflow-hidden border-4 border-white shadow-md hover:shadow-lg transition-shadow"
onClick={() => setCurrentPhotoIndex(index)}
whileHover={{ scale: 1.03 }}
transition={{ duration: 0.2 }}
>
<AnimatedImage
src={photo.url || "/placeholder.svg"}
alt={photo.caption || `Photo ${index + 1}`}
fill
/>
<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>
)}
</motion.button>
</DialogTrigger>
<DialogContent className="max-w-4xl p-0 bg-transparent border-none">
<AnimatePresence mode="wait">
<motion.div
key={currentPhotoIndex}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
className="relative bg-black rounded-lg overflow-hidden"
>
<Button
variant="ghost"
size="icon"
className="absolute top-2 right-2 z-10 text-white bg-black/50 hover:bg-black/70"
onClick={() => setIsDialogOpen(false)}
>
<X size={20} />
</Button>
<div className="relative h-[80vh] w-full">
<AnimatedImage
src={
photos[currentPhotoIndex].url || "/placeholder.svg"
}
alt={
photos[currentPhotoIndex].caption ||
`Photo ${currentPhotoIndex + 1}`
}
fill
className="object-contain"
/>
</div>
{photos.length > 1 && (
<>
<Button
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}
>
<ChevronLeft size={24} />
</Button>
<Button
variant="ghost"
size="icon"
className="absolute right-2 top-1/2 -translate-y-1/2 text-white bg-black/50 hover:bg-black/70"
onClick={handleNext}
>
<ChevronRight size={24} />
</Button>
</>
)}
{photos[currentPhotoIndex].caption && (
<div className="absolute bottom-0 left-0 right-0 bg-black/70 text-white p-4">
<p>{photos[currentPhotoIndex].caption}</p>
{photos[currentPhotoIndex].year && (
<p className="text-sm text-gray-300">
Year: {photos[currentPhotoIndex].year}
</p>
)}
</div>
)}
</motion.div>
</AnimatePresence>
</DialogContent>
</Dialog>
))}
</div>
</CardContent>
</Card>
);
}

View File

@ -1,57 +0,0 @@
import { Link } from "react-router-dom";
import type { RelatedMigrant } from "@/types/migrant";
import { Card, CardContent } from "@/components/ui/card";
import { motion } from "framer-motion";
import AnimatedImage from "@/components/ui/animated-image";
interface RelatedMigrantsProps {
migrants: RelatedMigrant[];
}
const RelatedMigrants = ({ migrants }: RelatedMigrantsProps) => {
return (
<Card className="overflow-hidden border border-gray-200 shadow-none rounded-md bg-white">
<CardContent>
<h2 className="text-xl font-bold mb-2 font-serif text-gray-800">
Migranti Correlati
</h2>
<div className="h-1 w-16 bg-gradient-to-r from-green-600 via-white to-red-600 mb-4" />
<div className="space-y-4">
{migrants.map((migrant, index) => (
<motion.div
key={migrant.id}
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.3, delay: index * 0.1 }}
>
<Link
to={`/migrant/${migrant.id}`}
className="flex items-center gap-3 p-2 rounded-md hover:bg-gray-100 transition-colors"
>
<div className="relative h-12 w-12 rounded-full overflow-hidden flex-shrink-0 border-2 border-white shadow-sm">
<AnimatedImage
src={
migrant.photoUrl || "/placeholder?height=100&width=100"
}
alt={`${migrant.firstName} ${migrant.lastName}`}
fill
/>
</div>
<div>
<h3 className="font-medium">
{migrant.firstName} {migrant.lastName}
</h3>
<p className="text-sm text-gray-500">
{migrant.relationship}
</p>
</div>
</Link>
</motion.div>
))}
</div>
</CardContent>
</Card>
);
};
export default RelatedMigrants;

View File

@ -4,7 +4,7 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { useMigrantsSearch } from "@/hooks/useMigrantsSearch"; import { useMigrantsSearch } from "@/hooks/useMigrantsSearch";
import { formatDate } from "@/utils/date";
export default function SearchResults() { export default function SearchResults() {
const { const {
migrants, migrants,
@ -29,7 +29,7 @@ export default function SearchResults() {
{["home", "about", "search", "stories", "contact"].map((path) => ( {["home", "about", "search", "stories", "contact"].map((path) => (
<Link <Link
key={path} key={path}
to={`/${path}`} to={`/`}
className="text-sm font-medium hover:underline underline-offset-4 capitalize" className="text-sm font-medium hover:underline underline-offset-4 capitalize"
> >
{path} {path}
@ -87,18 +87,14 @@ export default function SearchResults() {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="text-sm space-y-2"> <CardContent className="text-sm space-y-2">
<div className="flex justify-between">
<span className="font-medium">ID:</span>
<span>{migrant.person_id}</span>
</div>
<div className="flex justify-between"> <div className="flex justify-between">
<span className="font-medium">Date of Birth:</span> <span className="font-medium">Date of Birth:</span>
<span>{migrant.date_of_birth || "Unknown"}</span> <span>{formatDate(migrant.date_of_birth) || "Unknown"}</span>
</div> </div>
{migrant.place_of_birth && ( {migrant.place_of_birth && (
<div className="flex justify-between"> <div className="flex justify-between">
<span className="font-medium">Place of Birth:</span> <span className="font-medium">Place of Birth:</span>
<span>{migrant.place_of_birth}</span> <span>{formatDate(migrant.place_of_birth) || "Unknown"}</span>
</div> </div>
)} )}
</CardContent> </CardContent>

View File

@ -1,19 +1,33 @@
"use client" "use client"
import { useState } from "react" import { useState, useEffect } from "react"
import { BarChart3, Home, LogOut, Settings, Users, Menu, X, UserPlus, Shield } from "lucide-react" import { BarChart3, Home, LogOut, Settings, Users, Menu, X, UserPlus, Shield } from "lucide-react"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator" import { Separator } from "@/components/ui/separator"
import apiService from "@/services/apiService" import apiService from "@/services/apiService"
import LogoutDialog from "@/components/admin/migrant/Modal/LogoutDialog" import LogoutDialog from "@/components/admin/migrant/Modal/LogoutDialog"
import type { User } from "@/types/api"
export default function Sidebar() { export default function Sidebar() {
const [collapsed, setCollapsed] = useState(false)
const [mobileOpen, setMobileOpen] = useState(false) const [mobileOpen, setMobileOpen] = useState(false)
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false) const [logoutDialogOpen, setLogoutDialogOpen] = useState(false)
const [isSubmitting, setIsSubmitting] = useState(false) const [isSubmitting, setIsSubmitting] = useState(false)
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
async function loadUser() {
try {
const currentUser = await apiService.fetchCurrentUser();
console.log("Fetched user:", currentUser);
setUser(currentUser);
} catch (error) {
console.error("Failed to fetch user info:", error);
}
}
loadUser();
}, [])
const handleLogout = async () => { const handleLogout = async () => {
try { try {
setIsSubmitting(true) setIsSubmitting(true)
@ -65,8 +79,7 @@ export default function Sidebar() {
{/* Sidebar */} {/* Sidebar */}
<aside <aside
className={`bg-gray-900 border-r border-gray-800 shadow-2xl fixed h-full z-50 flex flex-col className={`bg-gray-900 border-r border-gray-800 shadow-2xl fixed h-full z-50 flex flex-col w-64
${collapsed ? "w-16" : "w-64"}
${mobileOpen ? "left-0" : "-left-full md:left-0"} ${mobileOpen ? "left-0" : "-left-full md:left-0"}
transition-all duration-300 transition-all duration-300
`} `}
@ -76,22 +89,15 @@ export default function Sidebar() {
{/* Sidebar header */} {/* Sidebar header */}
<div className="p-4 flex items-center justify-center border-b border-gray-800 flex-shrink-0"> <div className="p-4 flex items-center justify-center border-b border-gray-800 flex-shrink-0">
{!collapsed && ( <div className="flex items-center">
<div className="flex items-center"> <div className="size-10 rounded-xl bg-[#9B2335] flex items-center justify-center mr-3 shadow-xl border border-gray-700">
<div className="size-10 rounded-xl bg-[#9B2335] flex items-center justify-center mr-3 shadow-xl border border-gray-700">
<Shield className="size-5 text-white" />
</div>
<div className="flex flex-col">
<h1 className="text-lg font-bold text-white">Italian Migrants</h1>
<p className="text-xs text-[#9B2335]">Admin Portal</p>
</div>
</div>
)}
{collapsed && (
<div className="size-10 rounded-xl bg-[#9B2335] flex items-center justify-center shadow-xl border border-gray-700">
<Shield className="size-5 text-white" /> <Shield className="size-5 text-white" />
</div> </div>
)} <div className="flex flex-col">
<h1 className="text-lg font-bold text-white">Italian Migrants</h1>
<p className="text-xs text-[#9B2335]">Admin Portal</p>
</div>
</div>
</div> </div>
{/* Scrollable navigation area */} {/* Scrollable navigation area */}
@ -107,8 +113,8 @@ export default function Sidebar() {
: "text-gray-300 hover:text-white hover:bg-gray-800" : "text-gray-300 hover:text-white hover:bg-gray-800"
}`} }`}
> >
<Home className={`${collapsed ? "mr-0" : "mr-3"} size-5`} /> <Home className="mr-3 size-5" />
{!collapsed && <span className="font-medium">Dashboard</span>} <span className="font-medium">Dashboard</span>
</Button> </Button>
</Link> </Link>
</li> </li>
@ -122,8 +128,8 @@ export default function Sidebar() {
: "text-gray-300 hover:text-white hover:bg-gray-800" : "text-gray-300 hover:text-white hover:bg-gray-800"
}`} }`}
> >
<Users className={`${collapsed ? "mr-0" : "mr-3"} size-5`} /> <Users className="mr-3 size-5" />
{!collapsed && <span className="font-medium">Migrants</span>} <span className="font-medium">Migrants</span>
</Button> </Button>
</Link> </Link>
</li> </li>
@ -137,8 +143,8 @@ export default function Sidebar() {
: "text-gray-300 hover:text-white hover:bg-gray-800" : "text-gray-300 hover:text-white hover:bg-gray-800"
}`} }`}
> >
<BarChart3 className={`${collapsed ? "mr-0" : "mr-3"} size-5`} /> <BarChart3 className="mr-3 size-5" />
{!collapsed && <span className="font-medium">Reports</span>} <span className="font-medium">Reports</span>
</Button> </Button>
</Link> </Link>
</li> </li>
@ -157,8 +163,8 @@ export default function Sidebar() {
: "text-gray-300 hover:text-white hover:bg-gray-800" : "text-gray-300 hover:text-white hover:bg-gray-800"
}`} }`}
> >
<UserPlus className={`${collapsed ? "mr-0" : "mr-3"} size-5`} /> <UserPlus className="mr-3 size-5" />
{!collapsed && <span className="font-medium">Create User</span>} <span className="font-medium">Create User</span>
</Button> </Button>
</Link> </Link>
</li> </li>
@ -172,8 +178,8 @@ export default function Sidebar() {
: "text-gray-300 hover:text-white hover:bg-gray-800" : "text-gray-300 hover:text-white hover:bg-gray-800"
}`} }`}
> >
<Settings className={`${collapsed ? "mr-0" : "mr-3"} size-5`} /> <Settings className="mr-3 size-5" />
{!collapsed && <span className="font-medium">Settings</span>} <span className="font-medium">Settings</span>
</Button> </Button>
</Link> </Link>
</li> </li>
@ -182,16 +188,30 @@ export default function Sidebar() {
{/* Sidebar footer */} {/* Sidebar footer */}
<div className="p-4 border-t border-gray-800 flex-shrink-0"> <div className="p-4 border-t border-gray-800 flex-shrink-0">
{/* User info display */}
{user && (
<div className="mb-3 p-3 bg-gray-800 rounded-lg">
<div className="flex items-center space-x-2 mb-1">
<div className="size-8 rounded-full bg-[#9B2335] flex items-center justify-center">
<span className="text-xs font-bold text-white">{user.name.charAt(0).toUpperCase()}</span>
</div>
<div className="text-sm">
<p className="font-medium text-white truncate">{user.name}</p>
<p className="text-gray-400 text-xs truncate">{user.email}</p>
</div>
</div>
</div>
)}
<Button <Button
variant="ghost" variant="ghost"
className="w-full justify-start text-red-400 hover:bg-red-900/20 hover:text-red-300 rounded-lg transition-all duration-300" className="w-full justify-start text-red-400 hover:bg-red-900/20 hover:text-red-300 rounded-lg transition-all duration-300"
onClick={() => setLogoutDialogOpen(true)} onClick={() => setLogoutDialogOpen(true)}
> >
<LogOut className={`${collapsed ? "mr-0" : "mr-3"} size-5`} /> <LogOut className="mr-3 size-5" />
{!collapsed && <span className="font-medium">Logout</span>} <span className="font-medium">Logout</span>
</Button> </Button>
</div> </div>
</aside> </aside>
</> </>
) )
} }

View File

@ -4,37 +4,19 @@ import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
const Dialog = React.forwardRef< const Dialog = DialogPrimitive.Root
React.ElementRef<typeof DialogPrimitive.Root>, Dialog.displayName = DialogPrimitive.Root.displayName
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root>
>((props, ref) => {
return <DialogPrimitive.Root data-slot="dialog" ref={ref} {...props} />
})
Dialog.displayName = "Dialog"
const DialogTrigger = React.forwardRef< const DialogTrigger = DialogPrimitive.Trigger
React.ElementRef<typeof DialogPrimitive.Trigger>, DialogTrigger.displayName = DialogPrimitive.Trigger.displayName
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>
>((props, ref) => {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" ref={ref} {...props} />
})
DialogTrigger.displayName = "DialogTrigger"
const DialogPortal = React.forwardRef< const DialogPortal = ({ ...props }: DialogPrimitive.DialogPortalProps) => (
React.ElementRef<typeof DialogPrimitive.Portal>, <DialogPrimitive.Portal {...props} />
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Portal> )
>((props, ref) => { DialogPortal.displayName = DialogPrimitive.Portal.displayName
return <DialogPrimitive.Portal data-slot="dialog-portal" ref={ref} {...props} />
})
DialogPortal.displayName = "DialogPortal"
const DialogClose = React.forwardRef< const DialogClose = DialogPrimitive.Close
React.ElementRef<typeof DialogPrimitive.Close>, DialogClose.displayName = DialogPrimitive.Close.displayName
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>
>((props, ref) => {
return <DialogPrimitive.Close data-slot="dialog-close" ref={ref} {...props} />
})
DialogClose.displayName = "DialogClose"
const DialogOverlay = React.forwardRef< const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>, React.ElementRef<typeof DialogPrimitive.Overlay>,
@ -52,7 +34,7 @@ const DialogOverlay = React.forwardRef<
/> />
) )
}) })
DialogOverlay.displayName = "DialogOverlay" DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef< const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>, React.ElementRef<typeof DialogPrimitive.Content>,
@ -71,7 +53,7 @@ const DialogContent = React.forwardRef<
{...props} {...props}
> >
{children} {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"> <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-none disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon /> <XIcon />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</DialogPrimitive.Close> </DialogPrimitive.Close>
@ -79,7 +61,7 @@ const DialogContent = React.forwardRef<
</DialogPortal> </DialogPortal>
) )
}) })
DialogContent.displayName = "DialogContent" DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = React.forwardRef< const DialogHeader = React.forwardRef<
HTMLDivElement, HTMLDivElement,
@ -121,7 +103,7 @@ const DialogTitle = React.forwardRef<
{...props} {...props}
/> />
)) ))
DialogTitle.displayName = "DialogTitle" DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef< const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>, React.ElementRef<typeof DialogPrimitive.Description>,
@ -134,7 +116,7 @@ const DialogDescription = React.forwardRef<
{...props} {...props}
/> />
)) ))
DialogDescription.displayName = "DialogDescription" DialogDescription.displayName = DialogPrimitive.Description.displayName
export { export {
Dialog, Dialog,

View File

@ -1,5 +1,5 @@
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { Toaster as Sonner, ToasterProps } from "sonner" import { Toaster as Sonner, type ToasterProps } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ToasterProps) => {
const { theme = "system" } = useTheme() const { theme = "system" } = useTheme()

View File

@ -1,35 +0,0 @@
"use client"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/hooks/useToast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@ -149,13 +149,9 @@ export const useTabsPaneFormSubmit = ({
Object.entries(family).forEach(([key, value]) => formData.append(`family[${key}]`, value)) Object.entries(family).forEach(([key, value]) => formData.append(`family[${key}]`, value))
Object.entries(internment).forEach(([key, value]) => formData.append(`internment[${key}]`, value)) Object.entries(internment).forEach(([key, value]) => formData.append(`internment[${key}]`, value))
// Add new photos
photos.forEach((file) => formData.append("photos[]", file)) photos.forEach((file) => formData.append("photos[]", file))
// Add all captions (existing + new)
captions.forEach((caption, index) => formData.append(`captions[${index}]`, caption)) captions.forEach((caption, index) => formData.append(`captions[${index}]`, caption))
// Add main photo index and profile photo info
if (mainPhotoIndex !== null) { if (mainPhotoIndex !== null) {
formData.append("main_photo_index", mainPhotoIndex.toString()) formData.append("main_photo_index", mainPhotoIndex.toString())
formData.append("set_as_profile", "true") formData.append("set_as_profile", "true")

View File

@ -8,8 +8,9 @@ class ApiService {
this.api = axios.create({ this.api = axios.create({
baseURL: import.meta.env.VITE_API_URL, baseURL: import.meta.env.VITE_API_URL,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
withCredentials: true, // IMPORTANT
}); });
// Request Interceptor // Request Interceptor
this.api.interceptors.request.use((config) => { this.api.interceptors.request.use((config) => {
const token = localStorage.getItem("token"); const token = localStorage.getItem("token");
@ -37,11 +38,13 @@ class ApiService {
// --- AUTH --- // --- AUTH ---
async login(params: { email: string; password: string }) { async login(params: { email: string; password: string }) {
return this.api.post("/api/login", params).then((res) => { return this.api.post("/api/login", params).then((res) => {
console.log("Token:", res.data.token);
localStorage.setItem("token", res.data.token); localStorage.setItem("token", res.data.token);
localStorage.setItem("user", JSON.stringify(res.data.user)); localStorage.setItem("user", JSON.stringify(res.data.user));
return res.data; return res.data;
}); });
} }
async register(params: { name: string; email: string; password: string }) { async register(params: { name: string; email: string; password: string }) {
return this.api.post("/api/register", params).then((res) => res.data); return this.api.post("/api/register", params).then((res) => res.data);
@ -50,7 +53,13 @@ class ApiService {
async createUser(user: User) { async createUser(user: User) {
return this.api.post("/api/register", user).then((res) => res.data); return this.api.post("/api/register", user).then((res) => res.data);
} }
async updateUser(user: User) {
return this.api.put("/api/user/account", user).then(res => res.data);
}
async displayAllUsers() {
return this.api.get("/api/users").then(res => res.data.data);
}
async logout() { async logout() {
return this.api.post("/api/logout").then((res) => { return this.api.post("/api/logout").then((res) => {
localStorage.removeItem("token"); localStorage.removeItem("token");
@ -58,6 +67,10 @@ class ApiService {
return res.data; return res.data;
}); });
} }
async fetchCurrentUser(): Promise<User> {
return this.api.get("/api/user").then((res) => res.data.data.user);
}
// --- MIGRANTS --- // --- MIGRANTS ---
async getMigrants(page = 1, perPage = 10, filters = {}): Promise<Person> { async getMigrants(page = 1, perPage = 10, filters = {}): Promise<Person> {

View File

@ -1,28 +1,30 @@
export interface User { export interface User {
name: string; name: string;
email: string; email: string;
password: string; password?: string;
password_confirmation: string; current_password?: string;
} password_confirmation?: string;
}
export interface Migration { export interface Migration {
migration_id?: number; migration_id?: number;
person_id?: number; person_id?: number;
date_of_arrival_aus?: string; date_of_arrival_aus: string;
date_of_arrival_nt?: string; date_of_arrival_nt: string;
arrival_period?: string; arrival_period: string;
data_source?: string; data_source: string;
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
} }
export interface Naturalization { export interface Naturalization {
naturalization_id?: number; naturalization_id?: string;
person_id?: number; person_id?: string;
date_of_naturalisation?: string; date_of_naturalisation: string;
no_of_cert?: string; no_of_cert: string;
issued_at?: string; issued_at: string;
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
} }
@ -30,8 +32,8 @@ export interface Naturalization {
export interface Residence { export interface Residence {
residence_id?: number; residence_id?: number;
person_id?: number; person_id?: number;
town_or_city?: string; town_or_city: string;
home_at_death?: string; home_at_death: string;
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
} }
@ -39,8 +41,8 @@ export interface Residence {
export interface Family { export interface Family {
family_id?: number; family_id?: number;
person_id?: number; person_id?: number;
names_of_parents?: string; names_of_parents: string;
names_of_children?: string; names_of_children: string;
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
} }
@ -48,12 +50,12 @@ export interface Family {
export interface Internment { export interface Internment {
internment_id?: number; internment_id?: number;
person_id?: number; person_id?: number;
corps_issued?: string; corps_issued: string;
interned_in?: string; interned_in: string;
sent_to?: string; sent_to: string;
internee_occupation?: string; internee_occupation: string;
internee_address?: string; internee_address: string;
cav?: string; cav: string;
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
} }
@ -75,17 +77,17 @@ export interface Photo {
export interface Person { export interface Person {
person_id: number; person_id?: number;
surname?: string; surname: string;
christian_name?: string; christian_name: string;
full_name?: string; full_name?: string;
date_of_birth?: string; date_of_birth: string;
place_of_birth?: string; place_of_birth: string;
date_of_death?: string; date_of_death: string;
occupation?: string; occupation: string;
additional_notes?: string; additional_notes: string;
reference?: string; reference: string;
id_card_no?: string; id_card_no: string;
created_at?: string; created_at?: string;
updated_at?: string; updated_at?: string;
@ -97,20 +99,12 @@ export interface Person {
residence?: Residence | null; residence?: Residence | null;
family?: Family | null; family?: Family | null;
internment?: Internment | null; internment?: Internment | null;
data: Person[]; data?: Person[];
total: number; total?: number;
last_page?: number;
current_page?: number;
} }
export interface PaginatedResponse<T> {
data: T[];
current_page: number;
last_page: number;
total: number;
per_page: number;
}
export interface createMigrantPayload { export interface createMigrantPayload {
surname: string; surname: string;
christian_name: string; christian_name: string;
@ -176,3 +170,12 @@ export interface ActivityLog {
created_at: string; created_at: string;
} }
export interface ExistingPhoto {
id: number
caption?: string
is_profile_photo?: boolean
filename?: string
url?: string
}