refactor: migrate to protected routes and enhance UI components
This commit is contained in:
parent
5d1c3576cf
commit
a340ab6abc
|
|
@ -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!"
|
||||
|
|
@ -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!"
|
||||
|
||||
|
||||
|
||||
|
||||
153
src/App.tsx
153
src/App.tsx
|
|
@ -1,47 +1,144 @@
|
|||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||
// import { Toaster } from "react-hot-toast"; // ✅ Import Toaster
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";
|
||||
import { useState, useEffect, type ReactNode } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import "./App.css";
|
||||
|
||||
// import MigrantProfilePage from "./pages/MigrantProfilePage";
|
||||
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";
|
||||
// Pages
|
||||
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 AdminDashboardPage from "./components/admin/AdminDashboard";
|
||||
import Migrants from "./components/admin/Migrants";
|
||||
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 SearchResults from "./components/home/SearchResults";
|
||||
import Sample from "./components/admin/Table";
|
||||
import SettingsPage from "./components/admin/Setting";
|
||||
import ProfileSettings from "./components/ui/ProfileSettings";
|
||||
import ReportsPage from "./components/admin/Reports";
|
||||
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 { 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() {
|
||||
return (
|
||||
<Router>
|
||||
{/* ✅ Add the Toaster at root level so it works everywhere */}
|
||||
<Toaster position="top-right" reverseOrder={false} />
|
||||
<Routes>
|
||||
{/* 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="/residence-chart" element={<ResidenceChart />} />
|
||||
<Route path="/search-results" element={<SearchResults />} />
|
||||
<Route path="/sample" element={<Sample />} />
|
||||
<Route path="/admin/settings/profile" element={<ProfileSettings />} />
|
||||
<Route path="/admin/migrants" element={<Migrants />} />
|
||||
<Route path="/admin" element={<AdminDashboardPage />} />
|
||||
<Route path="/admin/migrants/add" element={<AddMigrantPage />} />
|
||||
<Route path="/admin/settings" element={<SettingsPage />} />
|
||||
<Route path="/admin/reports" element={<ReportsPage />} />
|
||||
<Route path="/admin/migrants/edit/:id" element={<EditMigrant />} />
|
||||
<Route path="/admin/users/create" element={<UserCreate />} />
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route path="/register" element={<RegisterPage />} />
|
||||
<Route path="/" element={<HomePage />} />
|
||||
|
||||
{/* Auth Routes */}
|
||||
<Route path="/login" element={<PublicRoute><LoginPage /></PublicRoute>} />
|
||||
<Route path="/register" element={<PublicRoute><RegisterPage /></PublicRoute>} />
|
||||
|
||||
{/* Admin Protected Routes */}
|
||||
<Route path="/admin" element={<ProtectedRoute><AdminDashboardPage /></ProtectedRoute>} />
|
||||
<Route path="/admin/migrants" element={<ProtectedRoute><Migrants /></ProtectedRoute>} />
|
||||
<Route path="/admin/migrants/add" element={<ProtectedRoute><AddMigrantPage /></ProtectedRoute>} />
|
||||
<Route path="/admin/migrants/edit/:id" element={<ProtectedRoute><EditMigrant /></ProtectedRoute>} />
|
||||
<Route path="/admin/settings" element={<ProtectedRoute><SettingsPage /></ProtectedRoute>} />
|
||||
<Route path="/admin/settings/profile" element={<ProtectedRoute><ProfileSettings /></ProtectedRoute>} />
|
||||
<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 />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import React, { useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { Eye, EyeOff, Lock, Mail } from "lucide-react"
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
|
|
@ -20,22 +18,18 @@ export default function LoginPage() {
|
|||
const navigate = useNavigate()
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
e.preventDefault() // Prevent page reload
|
||||
setError("")
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const response = await apiService.login({
|
||||
email,
|
||||
password,
|
||||
})
|
||||
console.log("Response:", response.data)
|
||||
alert("Login successful!")
|
||||
const response = await apiService.login({ email, password })
|
||||
console.log("Response:", response)
|
||||
navigate("/admin")
|
||||
} catch (error: any) {
|
||||
console.error("Error submitting form:", error)
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
setError(error.response.data.message)
|
||||
} catch (err: any) {
|
||||
console.error("Login error:", err)
|
||||
if (err.response?.data?.message) {
|
||||
setError(err.response.data.message)
|
||||
} else {
|
||||
setError("Login failed. Please check your input and try again.")
|
||||
}
|
||||
|
|
@ -65,9 +59,11 @@ export default function LoginPage() {
|
|||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
autoComplete="username"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<div className="relative">
|
||||
|
|
@ -79,18 +75,27 @@ export default function LoginPage() {
|
|||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute right-3 top-2.5 text-gray-500 hover:text-gray-700"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
aria-label={showPassword ? "Hide password" : "Show password"}
|
||||
>
|
||||
{showPassword ? <EyeOff className="size-5" /> : <Eye className="size-5" />}
|
||||
</button>
|
||||
</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>
|
||||
|
||||
<CardFooter>
|
||||
<Button type="submit" className="w-full mt-4" disabled={isLoading}>
|
||||
{isLoading ? "Authenticating..." : "Sign In"}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
import type React from "react"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
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 { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
|
@ -13,16 +13,42 @@ import { toast } from "react-hot-toast"
|
|||
import apiService from "@/services/apiService"
|
||||
import Header from "@/components/layout/Header"
|
||||
import Sidebar from "@/components/layout/Sidebar"
|
||||
import type { User } from "@/types/api"
|
||||
|
||||
export default function UserCreate() {
|
||||
const navigate = useNavigate()
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [formData, setFormData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
password: "",
|
||||
password_confirmation: "",
|
||||
})
|
||||
const [user, setUser] = useState<User | null>(null)
|
||||
const [formData, setFormData] = useState<User>({
|
||||
name: '',
|
||||
email: '',
|
||||
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 { name, value } = e.target
|
||||
|
|
@ -32,35 +58,47 @@ export default function UserCreate() {
|
|||
}))
|
||||
}
|
||||
|
||||
// In your React component:
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
e.preventDefault();
|
||||
|
||||
// Basic validation
|
||||
if (!formData.name || !formData.email || !formData.password) {
|
||||
toast.error("Please fill in all required fields")
|
||||
return
|
||||
if (!formData.name || !formData.email || !formData.current_password) {
|
||||
toast.error("Please fill in all required fields");
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.password !== formData.password_confirmation) {
|
||||
toast.error("Passwords don't match")
|
||||
return
|
||||
if (formData.password && formData.password !== formData.password_confirmation) {
|
||||
toast.error("Passwords don't match");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true)
|
||||
setIsSubmitting(true);
|
||||
|
||||
// This would need to be implemented in your apiService
|
||||
await apiService.createUser(formData)
|
||||
// Call updateUser instead of createUser
|
||||
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!")
|
||||
navigate("/admin/settings") // Redirect to an appropriate page
|
||||
} catch (error) {
|
||||
console.error("Error creating user:", error)
|
||||
toast.error("Failed to create user. Please try again.")
|
||||
toast.success(response.message || "User updated successfully!");
|
||||
navigate("/admin/settings");
|
||||
} catch (error: any) {
|
||||
console.error("Error updating user:", error);
|
||||
|
||||
if (error.response?.data?.message) {
|
||||
toast.error(error.response.data.message);
|
||||
} else {
|
||||
toast.error("Failed to update user. Please try again.");
|
||||
}
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<div className="flex min-h-dvh bg-gray-950">
|
||||
|
|
@ -82,6 +120,40 @@ export default function UserCreate() {
|
|||
<p className="text-gray-400 mt-2 ml-10">Manage your profile and security preferences</p>
|
||||
</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">
|
||||
<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>
|
||||
|
|
@ -135,9 +207,12 @@ export default function UserCreate() {
|
|||
name="current_password"
|
||||
type="password"
|
||||
placeholder="Enter current password"
|
||||
value={formData.current_password}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
className="bg-gray-800 border-gray-700 text-white focus:border-[#9B2335] placeholder:text-gray-500"
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
|
|
|||
|
|
@ -11,11 +11,11 @@ import { Progress } from "@/components/ui/progress"
|
|||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import type { Person, Migration, Naturalization, Residence, Family, Internment, ExistingPhoto } from "@/types/api"
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Save,
|
||||
Edit3,
|
||||
User,
|
||||
MapPin,
|
||||
FileText,
|
||||
|
|
@ -25,7 +25,6 @@ import {
|
|||
Camera,
|
||||
} from "lucide-react"
|
||||
import apiService from "@/services/apiService"
|
||||
import { useTabsPaneFormSubmit } from "@/hooks/useTabsPaneFormSubmit"
|
||||
import { showSuccessToast, showErrorToast, showUpdateItemToast } from "@/utils/toast"
|
||||
|
||||
// Import confirmation modals
|
||||
|
|
@ -57,59 +56,6 @@ const stepDescriptions = [
|
|||
|
||||
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 { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
|
|
@ -126,7 +72,7 @@ const StepperForm = () => {
|
|||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
|
||||
// Initial state functions
|
||||
const getInitialPersonState = (): PersonData => ({
|
||||
const getInitialPersonState = (): Person => ({
|
||||
surname: "",
|
||||
christian_name: "",
|
||||
date_of_birth: "",
|
||||
|
|
@ -136,32 +82,33 @@ const StepperForm = () => {
|
|||
additional_notes: "",
|
||||
reference: "",
|
||||
id_card_no: "",
|
||||
person_id: 0,
|
||||
})
|
||||
|
||||
const getInitialMigrationState = (): MigrationData => ({
|
||||
const getInitialMigrationState = (): Migration => ({
|
||||
date_of_arrival_aus: "",
|
||||
date_of_arrival_nt: "",
|
||||
arrival_period: "",
|
||||
data_source: "",
|
||||
})
|
||||
|
||||
const getInitialNaturalizationState = (): NaturalizationData => ({
|
||||
const getInitialNaturalizationState = (): Naturalization => ({
|
||||
date_of_naturalisation: "",
|
||||
no_of_cert: "",
|
||||
issued_at: "",
|
||||
})
|
||||
|
||||
const getInitialResidenceState = (): ResidenceData => ({
|
||||
const getInitialResidenceState = (): Residence => ({
|
||||
town_or_city: "",
|
||||
home_at_death: "",
|
||||
})
|
||||
|
||||
const getInitialFamilyState = (): FamilyData => ({
|
||||
const getInitialFamilyState = (): Family => ({
|
||||
names_of_parents: "",
|
||||
names_of_children: "",
|
||||
})
|
||||
|
||||
const getInitialInternmentState = (): InternmentData => ({
|
||||
const getInitialInternmentState = (): Internment => ({
|
||||
corps_issued: "",
|
||||
interned_in: "",
|
||||
sent_to: "",
|
||||
|
|
@ -171,12 +118,12 @@ const StepperForm = () => {
|
|||
})
|
||||
|
||||
// Form data state
|
||||
const [person, setPerson] = useState<PersonData>(getInitialPersonState())
|
||||
const [migration, setMigration] = useState<MigrationData>(getInitialMigrationState())
|
||||
const [naturalization, setNaturalization] = useState<NaturalizationData>(getInitialNaturalizationState())
|
||||
const [residence, setResidence] = useState<ResidenceData>(getInitialResidenceState())
|
||||
const [family, setFamily] = useState<FamilyData>(getInitialFamilyState())
|
||||
const [internment, setInternment] = useState<InternmentData>(getInitialInternmentState())
|
||||
const [person, setPerson] = useState<Person>(getInitialPersonState())
|
||||
const [migration, setMigration] = useState<Migration>(getInitialMigrationState())
|
||||
const [naturalization, setNaturalization] = useState<Naturalization>(getInitialNaturalizationState())
|
||||
const [residence, setResidence] = useState<Residence>(getInitialResidenceState())
|
||||
const [family, setFamily] = useState<Family>(getInitialFamilyState())
|
||||
const [internment, setInternment] = useState<Internment>(getInitialInternmentState())
|
||||
|
||||
// Photo state
|
||||
const [photos, setPhotos] = useState<File[]>([])
|
||||
|
|
@ -215,7 +162,7 @@ const StepperForm = () => {
|
|||
|
||||
const migrantData = await apiService.getMigrantById(personId)
|
||||
|
||||
// Populate person data
|
||||
// FIX: Populate person data from migrantData.person, not current person state
|
||||
setPerson({
|
||||
surname: migrantData.surname || "",
|
||||
christian_name: migrantData.christian_name || "",
|
||||
|
|
@ -226,6 +173,7 @@ const StepperForm = () => {
|
|||
additional_notes: migrantData.additional_notes || "",
|
||||
reference: migrantData.reference || "",
|
||||
id_card_no: migrantData.id_card_no || "",
|
||||
person_id: migrantData.person_id || 0,
|
||||
})
|
||||
|
||||
// Populate migration data
|
||||
|
|
@ -275,7 +223,6 @@ const StepperForm = () => {
|
|||
})
|
||||
}
|
||||
|
||||
// Handle existing photos with better logic
|
||||
if (migrantData.photos && Array.isArray(migrantData.photos) && migrantData.photos.length > 0) {
|
||||
const photoData = migrantData.photos as ExistingPhoto[]
|
||||
setExistingPhotos(photoData)
|
||||
|
|
@ -319,24 +266,19 @@ const StepperForm = () => {
|
|||
if (e.target.files && e.target.files.length > 0) {
|
||||
const selectedFiles = Array.from(e.target.files)
|
||||
|
||||
// Add new photos
|
||||
setPhotos((prev) => [...prev, ...selectedFiles])
|
||||
|
||||
// Create previews for new photos
|
||||
const newPreviews = selectedFiles.map((file) => URL.createObjectURL(file))
|
||||
setPhotoPreviews((prev) => [...prev, ...newPreviews])
|
||||
|
||||
// Add captions for new photos
|
||||
setCaptions((prev) => {
|
||||
const newCaptions = [...prev]
|
||||
// Add empty captions for new photos
|
||||
for (let i = 0; i < selectedFiles.length; i++) {
|
||||
newCaptions.push("")
|
||||
}
|
||||
return newCaptions
|
||||
})
|
||||
|
||||
// Set main photo index if none exists and we have photos
|
||||
if (mainPhotoIndex === null && (existingPhotos.length > 0 || selectedFiles.length > 0)) {
|
||||
setMainPhotoIndex(0)
|
||||
}
|
||||
|
|
@ -349,7 +291,6 @@ const StepperForm = () => {
|
|||
const removeExistingPhoto = (index: number) => {
|
||||
const wasMainPhoto = mainPhotoIndex === index
|
||||
|
||||
// Remove the photo from existing photos
|
||||
setExistingPhotos((prev) => prev.filter((_, i) => i !== index))
|
||||
|
||||
// Remove corresponding caption
|
||||
|
|
@ -408,13 +349,10 @@ const StepperForm = () => {
|
|||
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
// Add person data
|
||||
console.log('Person data:', person);
|
||||
|
||||
|
||||
Object.entries(person).forEach(([key, value]) => {
|
||||
if (value) formData.append(key, value)
|
||||
})
|
||||
|
|
@ -458,46 +396,99 @@ const StepperForm = () => {
|
|||
})
|
||||
}
|
||||
|
||||
// Add new photos
|
||||
// Add photos
|
||||
console.log('Photos:', photos);
|
||||
photos.forEach((photo) => {
|
||||
formData.append('photos[]', photo)
|
||||
})
|
||||
|
||||
// Add captions
|
||||
console.log('Captions:', captions);
|
||||
captions.forEach((caption, index) => {
|
||||
if (caption) {
|
||||
formData.append(`captions[${index}]`, caption)
|
||||
}
|
||||
// FIXED: Add captions for new photos only (existing photo captions handled separately)
|
||||
console.log('New photo captions:');
|
||||
const newPhotoCaptions = captions.slice(existingPhotos.length); // Get captions for new photos only
|
||||
newPhotoCaptions.forEach((caption, index) => {
|
||||
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('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) {
|
||||
formData.append('set_as_profile', 'true')
|
||||
|
||||
console.log('Existing photos:', existingPhotos);
|
||||
if (mainPhotoIndex < existingPhotos.length) {
|
||||
// Main photo is an existing photo
|
||||
const existingPhoto = existingPhotos[mainPhotoIndex]
|
||||
console.log('Setting existing photo as main:', existingPhoto.id);
|
||||
formData.append('profile_photo_id', existingPhoto.id.toString())
|
||||
} else {
|
||||
// Main photo is a new photo
|
||||
const newPhotoIndex = mainPhotoIndex - existingPhotos.length
|
||||
console.log('New photo index:', newPhotoIndex);
|
||||
console.log('Setting new photo as main, index:', newPhotoIndex);
|
||||
|
||||
// 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)
|
||||
console.log('Existing photos:', existingPhotos);
|
||||
// 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())
|
||||
if (captions[index]) {
|
||||
formData.append(`existing_photos[${index}][caption]`, captions[index])
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
let response
|
||||
if (isEditMode && id) {
|
||||
|
|
|
|||
|
|
@ -65,12 +65,11 @@ const PhotosStep: React.FC<PhotosStepProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Existing Photos */}
|
||||
{existingPhotos.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
)}
|
||||
|
||||
{/* New Photos */}
|
||||
{photos.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
|
|
|
|||
|
|
@ -1,34 +1,46 @@
|
|||
|
||||
|
||||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
|
||||
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 { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "@/components/ui/carousel"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import SearchForm from "./SearchForm"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import ApiService from "@/services/apiService";
|
||||
import type { Person } from "@/types/api";
|
||||
import ApiService from "@/services/apiService"
|
||||
import type { Person } from "@/types/api"
|
||||
|
||||
export default function Home() {
|
||||
const [currentSlide, setCurrentSlide] = useState(0)
|
||||
const navigate = useNavigate();
|
||||
const [total, setTotal] = useState(0);
|
||||
const [migrants, setMigrants] = useState<Person[]>([]);
|
||||
const API_BASE_URL = "http://localhost:8000";
|
||||
const navigate = useNavigate()
|
||||
const [total, setTotal] = useState(0)
|
||||
const [migrants, setMigrants] = useState<Person[]>([])
|
||||
const API_BASE_URL = "http://localhost:8000"
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
const response = await ApiService.getMigrants(1, 10);
|
||||
setMigrants(response.data);
|
||||
setTotal(response.total);
|
||||
const response = await ApiService.getMigrants(1, 10)
|
||||
setMigrants(response.data)
|
||||
setTotal(response.total)
|
||||
}
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
fetchData()
|
||||
}, [])
|
||||
|
||||
const backgroundImages = [
|
||||
{
|
||||
|
|
@ -49,7 +61,6 @@ export default function Home() {
|
|||
},
|
||||
]
|
||||
|
||||
|
||||
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",
|
||||
|
|
@ -80,6 +91,18 @@ export default function Home() {
|
|||
return () => clearInterval(timer)
|
||||
}, [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 (
|
||||
<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">
|
||||
|
|
@ -97,9 +120,13 @@ export default function Home() {
|
|||
<a href="#stories" className="text-sm font-medium text-white hover:text-white/80 transition-colors">
|
||||
Stories
|
||||
</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
|
||||
</Link>
|
||||
</a>
|
||||
</nav>
|
||||
<Button variant="outline" size="icon" className="md:hidden border-white/20 text-white hover:bg-white/10">
|
||||
<Search className="h-4 w-4" />
|
||||
|
|
@ -116,7 +143,8 @@ export default function Home() {
|
|||
{backgroundImages.map((image, index) => (
|
||||
<div
|
||||
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" />
|
||||
|
|
@ -141,7 +169,6 @@ export default function Home() {
|
|||
<SearchForm />
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
variant="outline"
|
||||
|
|
@ -240,7 +267,7 @@ export default function Home() {
|
|||
<CarouselContent>
|
||||
{migrants.map((person) => {
|
||||
// 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 (
|
||||
<CarouselItem key={person.person_id} className="md:basis-1/2 lg:basis-1/2">
|
||||
|
|
@ -286,7 +313,7 @@ export default function Home() {
|
|||
</Card>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
);
|
||||
)
|
||||
})}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious />
|
||||
|
|
@ -343,21 +370,212 @@ export default function Home() {
|
|||
</div>
|
||||
</section>
|
||||
</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">
|
||||
<p className="text-xs">© {new Date().getFullYear()} Italian Migrants NT. All rights reserved.</p>
|
||||
<nav className="sm:ml-auto flex gap-4 sm:gap-6">
|
||||
<Link to="/terms" className="text-xs hover:underline underline-offset-4">
|
||||
|
||||
{/* Enhanced Footer with Contact Information */}
|
||||
<footer id="contact" className="bg-[#1A2A57] text-white">
|
||||
<div className="container px-4 md:px-6">
|
||||
{/* Main Footer Content */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8 py-12">
|
||||
{/* About Section */}
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-lg font-bold font-serif text-[#E8DCCA]">Italian Migrants NT</h3>
|
||||
<p className="text-sm text-gray-300 leading-relaxed">
|
||||
Preserving and celebrating the rich heritage of Italian migrants to the Northern Territory through
|
||||
digital archives, personal stories, and historical documentation.
|
||||
</p>
|
||||
<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>© {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-xs hover:underline underline-offset-4">
|
||||
Privacy
|
||||
<Link to="/privacy" className="text-sm text-gray-300 hover:text-[#E8DCCA] transition-colors">
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link to="/admin" className="text-xs hover:underline underline-offset-4">
|
||||
<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>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -47,21 +47,15 @@ export default function MigrantProfile() {
|
|||
<span className="text-xl font-bold text-[#9B2335]">Italian Migrants NT</span>
|
||||
</Link>
|
||||
<nav className="hidden md:flex gap-6">
|
||||
<Link to="/" className="text-sm font-medium hover:underline underline-offset-4">
|
||||
Home
|
||||
</Link>
|
||||
<Link to="/about" className="text-sm font-medium hover:underline underline-offset-4">
|
||||
About
|
||||
</Link>
|
||||
<Link to="/search" className="text-sm font-medium hover:underline underline-offset-4">
|
||||
Search
|
||||
</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
|
||||
{["home", "about", "search", "stories", "contact"].map((path) => (
|
||||
<Link
|
||||
key={path}
|
||||
to={`/`}
|
||||
className="text-sm font-medium hover:underline underline-offset-4 capitalize"
|
||||
>
|
||||
{path}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -4,7 +4,7 @@ import {
|
|||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMigrantsSearch } from "@/hooks/useMigrantsSearch";
|
||||
|
||||
import { formatDate } from "@/utils/date";
|
||||
export default function SearchResults() {
|
||||
const {
|
||||
migrants,
|
||||
|
|
@ -29,7 +29,7 @@ export default function SearchResults() {
|
|||
{["home", "about", "search", "stories", "contact"].map((path) => (
|
||||
<Link
|
||||
key={path}
|
||||
to={`/${path}`}
|
||||
to={`/`}
|
||||
className="text-sm font-medium hover:underline underline-offset-4 capitalize"
|
||||
>
|
||||
{path}
|
||||
|
|
@ -87,18 +87,14 @@ export default function SearchResults() {
|
|||
</CardTitle>
|
||||
</CardHeader>
|
||||
<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">
|
||||
<span className="font-medium">Date of Birth:</span>
|
||||
<span>{migrant.date_of_birth || "Unknown"}</span>
|
||||
<span>{formatDate(migrant.date_of_birth) || "Unknown"}</span>
|
||||
</div>
|
||||
{migrant.place_of_birth && (
|
||||
<div className="flex justify-between">
|
||||
<span className="font-medium">Place of Birth:</span>
|
||||
<span>{migrant.place_of_birth}</span>
|
||||
<span>{formatDate(migrant.place_of_birth) || "Unknown"}</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,32 @@
|
|||
"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 { Link } from "react-router-dom"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Separator } from "@/components/ui/separator"
|
||||
import apiService from "@/services/apiService"
|
||||
import LogoutDialog from "@/components/admin/migrant/Modal/LogoutDialog"
|
||||
import type { User } from "@/types/api"
|
||||
|
||||
export default function Sidebar() {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const [logoutDialogOpen, setLogoutDialogOpen] = 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 () => {
|
||||
try {
|
||||
|
|
@ -65,8 +79,7 @@ export default function Sidebar() {
|
|||
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
className={`bg-gray-900 border-r border-gray-800 shadow-2xl fixed h-full z-50 flex flex-col
|
||||
${collapsed ? "w-16" : "w-64"}
|
||||
className={`bg-gray-900 border-r border-gray-800 shadow-2xl fixed h-full z-50 flex flex-col w-64
|
||||
${mobileOpen ? "left-0" : "-left-full md:left-0"}
|
||||
transition-all duration-300
|
||||
`}
|
||||
|
|
@ -76,7 +89,6 @@ export default function Sidebar() {
|
|||
|
||||
{/* Sidebar header */}
|
||||
<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="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" />
|
||||
|
|
@ -86,12 +98,6 @@ export default function Sidebar() {
|
|||
<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" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Scrollable navigation area */}
|
||||
|
|
@ -107,8 +113,8 @@ export default function Sidebar() {
|
|||
: "text-gray-300 hover:text-white hover:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<Home className={`${collapsed ? "mr-0" : "mr-3"} size-5`} />
|
||||
{!collapsed && <span className="font-medium">Dashboard</span>}
|
||||
<Home className="mr-3 size-5" />
|
||||
<span className="font-medium">Dashboard</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</li>
|
||||
|
|
@ -122,8 +128,8 @@ export default function Sidebar() {
|
|||
: "text-gray-300 hover:text-white hover:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<Users className={`${collapsed ? "mr-0" : "mr-3"} size-5`} />
|
||||
{!collapsed && <span className="font-medium">Migrants</span>}
|
||||
<Users className="mr-3 size-5" />
|
||||
<span className="font-medium">Migrants</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</li>
|
||||
|
|
@ -137,8 +143,8 @@ export default function Sidebar() {
|
|||
: "text-gray-300 hover:text-white hover:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<BarChart3 className={`${collapsed ? "mr-0" : "mr-3"} size-5`} />
|
||||
{!collapsed && <span className="font-medium">Reports</span>}
|
||||
<BarChart3 className="mr-3 size-5" />
|
||||
<span className="font-medium">Reports</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</li>
|
||||
|
|
@ -157,8 +163,8 @@ export default function Sidebar() {
|
|||
: "text-gray-300 hover:text-white hover:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<UserPlus className={`${collapsed ? "mr-0" : "mr-3"} size-5`} />
|
||||
{!collapsed && <span className="font-medium">Create User</span>}
|
||||
<UserPlus className="mr-3 size-5" />
|
||||
<span className="font-medium">Create User</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</li>
|
||||
|
|
@ -172,8 +178,8 @@ export default function Sidebar() {
|
|||
: "text-gray-300 hover:text-white hover:bg-gray-800"
|
||||
}`}
|
||||
>
|
||||
<Settings className={`${collapsed ? "mr-0" : "mr-3"} size-5`} />
|
||||
{!collapsed && <span className="font-medium">Settings</span>}
|
||||
<Settings className="mr-3 size-5" />
|
||||
<span className="font-medium">Settings</span>
|
||||
</Button>
|
||||
</Link>
|
||||
</li>
|
||||
|
|
@ -182,13 +188,27 @@ export default function Sidebar() {
|
|||
|
||||
{/* Sidebar footer */}
|
||||
<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
|
||||
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"
|
||||
onClick={() => setLogoutDialogOpen(true)}
|
||||
>
|
||||
<LogOut className={`${collapsed ? "mr-0" : "mr-3"} size-5`} />
|
||||
{!collapsed && <span className="font-medium">Logout</span>}
|
||||
<LogOut className="mr-3 size-5" />
|
||||
<span className="font-medium">Logout</span>
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
|
|
|
|||
|
|
@ -4,37 +4,19 @@ import { XIcon } from "lucide-react"
|
|||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Root>
|
||||
>((props, ref) => {
|
||||
return <DialogPrimitive.Root data-slot="dialog" ref={ref} {...props} />
|
||||
})
|
||||
Dialog.displayName = "Dialog"
|
||||
const Dialog = DialogPrimitive.Root
|
||||
Dialog.displayName = DialogPrimitive.Root.displayName
|
||||
|
||||
const DialogTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Trigger>
|
||||
>((props, ref) => {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" ref={ref} {...props} />
|
||||
})
|
||||
DialogTrigger.displayName = "DialogTrigger"
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
DialogTrigger.displayName = DialogPrimitive.Trigger.displayName
|
||||
|
||||
const DialogPortal = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Portal>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Portal>
|
||||
>((props, ref) => {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" ref={ref} {...props} />
|
||||
})
|
||||
DialogPortal.displayName = "DialogPortal"
|
||||
const DialogPortal = ({ ...props }: DialogPrimitive.DialogPortalProps) => (
|
||||
<DialogPrimitive.Portal {...props} />
|
||||
)
|
||||
DialogPortal.displayName = DialogPrimitive.Portal.displayName
|
||||
|
||||
const DialogClose = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>
|
||||
>((props, ref) => {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" ref={ref} {...props} />
|
||||
})
|
||||
DialogClose.displayName = "DialogClose"
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
DialogClose.displayName = DialogPrimitive.Close.displayName
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
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<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
|
|
@ -71,7 +53,7 @@ const DialogContent = React.forwardRef<
|
|||
{...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">
|
||||
<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 />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
|
|
@ -79,7 +61,7 @@ const DialogContent = React.forwardRef<
|
|||
</DialogPortal>
|
||||
)
|
||||
})
|
||||
DialogContent.displayName = "DialogContent"
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
|
|
@ -121,7 +103,7 @@ const DialogTitle = React.forwardRef<
|
|||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = "DialogTitle"
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
|
|
@ -134,7 +116,7 @@ const DialogDescription = React.forwardRef<
|
|||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = "DialogDescription"
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { theme = "system" } = useTheme()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -149,13 +149,9 @@ export const useTabsPaneFormSubmit = ({
|
|||
Object.entries(family).forEach(([key, value]) => formData.append(`family[${key}]`, value))
|
||||
Object.entries(internment).forEach(([key, value]) => formData.append(`internment[${key}]`, value))
|
||||
|
||||
// Add new photos
|
||||
photos.forEach((file) => formData.append("photos[]", file))
|
||||
|
||||
// Add all captions (existing + new)
|
||||
captions.forEach((caption, index) => formData.append(`captions[${index}]`, caption))
|
||||
|
||||
// Add main photo index and profile photo info
|
||||
if (mainPhotoIndex !== null) {
|
||||
formData.append("main_photo_index", mainPhotoIndex.toString())
|
||||
formData.append("set_as_profile", "true")
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ class ApiService {
|
|||
this.api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_URL,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
withCredentials: true, // IMPORTANT
|
||||
});
|
||||
|
||||
// Request Interceptor
|
||||
|
|
@ -37,12 +38,14 @@ class ApiService {
|
|||
// --- AUTH ---
|
||||
async login(params: { email: string; password: string }) {
|
||||
return this.api.post("/api/login", params).then((res) => {
|
||||
console.log("Token:", res.data.token);
|
||||
localStorage.setItem("token", res.data.token);
|
||||
localStorage.setItem("user", JSON.stringify(res.data.user));
|
||||
return res.data;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
async register(params: { name: string; email: string; password: string }) {
|
||||
return this.api.post("/api/register", params).then((res) => res.data);
|
||||
}
|
||||
|
|
@ -50,6 +53,12 @@ class ApiService {
|
|||
async createUser(user: User) {
|
||||
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() {
|
||||
return this.api.post("/api/logout").then((res) => {
|
||||
|
|
@ -58,6 +67,10 @@ class ApiService {
|
|||
return res.data;
|
||||
});
|
||||
}
|
||||
async fetchCurrentUser(): Promise<User> {
|
||||
return this.api.get("/api/user").then((res) => res.data.data.user);
|
||||
}
|
||||
|
||||
|
||||
// --- MIGRANTS ---
|
||||
async getMigrants(page = 1, perPage = 10, filters = {}): Promise<Person> {
|
||||
|
|
|
|||
|
|
@ -1,28 +1,30 @@
|
|||
export interface User {
|
||||
export interface User {
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
password_confirmation: string;
|
||||
}
|
||||
|
||||
password?: string;
|
||||
current_password?: string;
|
||||
password_confirmation?: string;
|
||||
}
|
||||
|
||||
export interface Migration {
|
||||
migration_id?: number;
|
||||
person_id?: number;
|
||||
date_of_arrival_aus?: string;
|
||||
date_of_arrival_nt?: string;
|
||||
arrival_period?: string;
|
||||
data_source?: string;
|
||||
date_of_arrival_aus: string;
|
||||
date_of_arrival_nt: string;
|
||||
arrival_period: string;
|
||||
data_source: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
|
||||
|
||||
}
|
||||
|
||||
export interface Naturalization {
|
||||
naturalization_id?: number;
|
||||
person_id?: number;
|
||||
date_of_naturalisation?: string;
|
||||
no_of_cert?: string;
|
||||
issued_at?: string;
|
||||
naturalization_id?: string;
|
||||
person_id?: string;
|
||||
date_of_naturalisation: string;
|
||||
no_of_cert: string;
|
||||
issued_at: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
|
@ -30,8 +32,8 @@ export interface Naturalization {
|
|||
export interface Residence {
|
||||
residence_id?: number;
|
||||
person_id?: number;
|
||||
town_or_city?: string;
|
||||
home_at_death?: string;
|
||||
town_or_city: string;
|
||||
home_at_death: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
|
@ -39,8 +41,8 @@ export interface Residence {
|
|||
export interface Family {
|
||||
family_id?: number;
|
||||
person_id?: number;
|
||||
names_of_parents?: string;
|
||||
names_of_children?: string;
|
||||
names_of_parents: string;
|
||||
names_of_children: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
|
@ -48,12 +50,12 @@ export interface Family {
|
|||
export interface Internment {
|
||||
internment_id?: number;
|
||||
person_id?: number;
|
||||
corps_issued?: string;
|
||||
interned_in?: string;
|
||||
sent_to?: string;
|
||||
internee_occupation?: string;
|
||||
internee_address?: string;
|
||||
cav?: string;
|
||||
corps_issued: string;
|
||||
interned_in: string;
|
||||
sent_to: string;
|
||||
internee_occupation: string;
|
||||
internee_address: string;
|
||||
cav: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
|
@ -75,17 +77,17 @@ export interface Photo {
|
|||
|
||||
|
||||
export interface Person {
|
||||
person_id: number;
|
||||
surname?: string;
|
||||
christian_name?: string;
|
||||
person_id?: number;
|
||||
surname: string;
|
||||
christian_name: string;
|
||||
full_name?: string;
|
||||
date_of_birth?: string;
|
||||
place_of_birth?: string;
|
||||
date_of_death?: string;
|
||||
occupation?: string;
|
||||
additional_notes?: string;
|
||||
reference?: string;
|
||||
id_card_no?: string;
|
||||
date_of_birth: string;
|
||||
place_of_birth: string;
|
||||
date_of_death: string;
|
||||
occupation: string;
|
||||
additional_notes: string;
|
||||
reference: string;
|
||||
id_card_no: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
|
||||
|
|
@ -97,20 +99,12 @@ export interface Person {
|
|||
residence?: Residence | null;
|
||||
family?: Family | null;
|
||||
internment?: Internment | null;
|
||||
data: Person[];
|
||||
total: number;
|
||||
data?: Person[];
|
||||
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 {
|
||||
surname: string;
|
||||
christian_name: string;
|
||||
|
|
@ -176,3 +170,12 @@ export interface ActivityLog {
|
|||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ExistingPhoto {
|
||||
id: number
|
||||
caption?: string
|
||||
is_profile_photo?: boolean
|
||||
filename?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue