Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,10 @@ const App: React.FC = () => {
if (data.species) {
const localSpecies = getSpecies();
const localSpeciesMap = new Map(localSpecies.map(s => [s.id, s]));
const serverSpeciesIds = new Set(data.species.map((s: any) => s.id as string));
const localOnlySpecies = localSpecies.filter(s => !serverSpeciesIds.has(s.id));
// Server is authoritative — only merge server records (plus cached imageUrls).
// Do NOT push local-only items back; items are pushed at creation/edit time.
// Local-only items are pushed at creation/edit time; this is a safety fallback.
const mergedSpecies = data.species.map((s: any) => ({
...s,
imageUrl: s.imageUrl || localSpeciesMap.get(s.id)?.imageUrl || undefined,
Expand Down Expand Up @@ -442,6 +444,8 @@ const App: React.FC = () => {
if (data.individuals) {
const localInds = getIndividuals();
const localIndMap = new Map(localInds.map(i => [i.id, i]));
const serverIndIds = new Set(data.individuals.map((i: any) => i.id as string));
const localOnlyInds = localInds.filter(i => !serverIndIds.has(i.id));
// Server is authoritative — only merge server records (plus cached imageUrls).
const mergedInds = data.individuals.map((i: any) => ({
...i,
Expand Down
92 changes: 92 additions & 0 deletions components/ConfirmModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React from 'react';
import { AlertTriangle, Loader2, Trash2, X } from 'lucide-react';

export interface ConfirmModalProps {
isOpen: boolean;
title: string;
message: React.ReactNode;
confirmLabel?: string;
cancelLabel?: string;
onConfirm: () => void;
onCancel: () => void;
isLoading?: boolean;
/** 'danger' = red button (default). 'warning' = amber button. */
variant?: 'danger' | 'warning';
}

const ConfirmModal: React.FC<ConfirmModalProps> = ({
isOpen,
title,
message,
confirmLabel = 'Delete',
cancelLabel = 'Cancel',
onConfirm,
onCancel,
isLoading = false,
variant = 'danger',
}) => {
if (!isOpen) return null;

const btnCls = variant === 'danger'
? 'bg-red-600 hover:bg-red-700 shadow-red-100'
: 'bg-amber-500 hover:bg-amber-600 shadow-amber-100';

const iconBg = variant === 'danger'
? 'bg-red-100 text-red-600'
: 'bg-amber-100 text-amber-600';

return (
<div className="fixed inset-0 z-[200] flex items-center justify-center p-4">
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
onClick={!isLoading ? onCancel : undefined}
/>

{/* Panel */}
<div className="relative bg-white rounded-2xl shadow-2xl w-full max-w-sm p-6 animate-in zoom-in-95 duration-150">
{/* Close button */}
<button
onClick={onCancel}
disabled={isLoading}
className="absolute top-4 right-4 text-slate-300 hover:text-slate-500 transition-colors disabled:opacity-40"
>
<X size={18} />
</button>

<div className="flex items-start gap-4">
<div className={`p-3 rounded-full flex-shrink-0 ${iconBg}`}>
<AlertTriangle size={20} />
</div>
<div className="flex-1 min-w-0 pr-4">
<h3 className="font-bold text-base text-slate-900">{title}</h3>
<div className="text-sm text-slate-500 mt-1 leading-relaxed">{message}</div>
</div>
</div>

<div className="flex gap-3 mt-6 justify-end">
<button
onClick={onCancel}
disabled={isLoading}
className="px-4 py-2 text-sm font-semibold text-slate-600 hover:bg-slate-100 rounded-xl transition-colors disabled:opacity-50"
>
{cancelLabel}
</button>
<button
onClick={onConfirm}
disabled={isLoading}
className={`px-5 py-2 text-sm font-bold text-white rounded-xl flex items-center gap-2 shadow-lg transition-all disabled:opacity-50 ${btnCls}`}
>
{isLoading
? <Loader2 size={15} className="animate-spin" />
: <Trash2 size={15} />
}
{confirmLabel}
</button>
</div>
</div>
</div>
);
};

export default ConfirmModal;
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 15 additions & 2 deletions pages/IndividualDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Individual, Species, WeightRecord, HealthRecord, GrowthRecord, Breeding
import { ArrowLeft, Scale, Activity, Syringe, Calendar, Plus, Stethoscope, Sprout, Camera, MapPin, Navigation, X, ChevronLeft, ChevronRight, Maximize2, Briefcase, Archive, Edit, Baby, Heart, ArrowRightLeft, ExternalLink, Fingerprint, Download, FileCode, Box, Trash2, Loader2, Upload, ImageIcon, Info } from 'lucide-react';
import { LineChart, Line, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts';
import { LanguageContext } from '../App';
import ConfirmModal from '../components/ConfirmModal';

declare const L: any;

Expand Down Expand Up @@ -39,6 +40,7 @@ const IndividualDetail: React.FC = () => {
const [showWeightModal, setShowWeightModal] = useState(false);
const [showHealthModal, setShowHealthModal] = useState(false);
const [galleryIndex, setGalleryIndex] = useState<number>(-1);
const [showDnaDeleteConfirm, setShowDnaDeleteConfirm] = useState(false);

// Form Media State
const [pendingLogImage, setPendingLogImage] = useState<string>('');
Expand Down Expand Up @@ -118,7 +120,7 @@ const IndividualDetail: React.FC = () => {
const ACCURACY_TARGET = 20;
let settled = false;

const handleMapClick = (e: L.LeafletMouseEvent) => {
const handleMapClick = (e: any) => {
const { lat, lng } = e.latlng;
setPendingLatLng({ lat, lng });
locationMarkerRef.current?.setLatLng([lat, lng]);
Expand Down Expand Up @@ -722,7 +724,7 @@ const IndividualDetail: React.FC = () => {
<span className="text-[10px] font-bold text-emerald-400 uppercase tracking-widest">Sequence Detected: {individual.dnaFileType || 'FASTA'}</span>
<div className="flex gap-2">
<button className="text-slate-400 hover:text-white transition-colors" title="Download Sequence"><Download size={16}/></button>
<button onClick={() => { if(confirm("Remove DNA data?")) setIndividual({...individual, dnaSequence: undefined}); }} className="text-slate-400 hover:text-red-400 transition-colors"><Trash2 size={16}/></button>
<button onClick={() => setShowDnaDeleteConfirm(true)} className="text-slate-400 hover:text-red-400 transition-colors"><Trash2 size={16}/></button>
</div>
</div>
<div className="bg-black/30 p-4 rounded font-mono text-[10px] text-emerald-600 break-all h-32 overflow-y-auto">
Expand Down Expand Up @@ -947,6 +949,17 @@ const IndividualDetail: React.FC = () => {
);
})()}

<ConfirmModal
isOpen={showDnaDeleteConfirm}
title="Remove DNA Data"
message="Permanently remove the stored DNA sequence for this individual? This cannot be undone."
confirmLabel="Remove"
onConfirm={() => {
if (individual) setIndividual({ ...individual, dnaSequence: undefined });
setShowDnaDeleteConfirm(false);
}}
onCancel={() => setShowDnaDeleteConfirm(false)}
/>
</div>
);
};
Expand Down
Loading
Loading