import { useKindeAuth } from "@kinde-oss/kinde-auth-react";
import React, { useEffect, useState } from "react";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./ui/table";
interface CallType {
id: string;
label: string;
}
interface Species {
id: string;
label: string;
ebirdCode: string | null;
description: string | null;
callTypes: CallType[];
}
interface SpeciesResponse {
data: Species[];
}
interface SpeciesProps {
datasetId: string;
}
// Function to assign a consistent shade of gray from the Stone palette
const getGrayShade = (str: string): string => {
// Simple hash function
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash = hash & hash; // Convert to 32bit integer
}
// Modulo to get a value between 0 and 4 for the gray shade
const shadeIndex = Math.abs(hash) % 5;
// Stone gray shades from shadcn palette (light to dark)
const shades = [
'bg-stone-50 text-stone-800',
'bg-stone-100 text-stone-800',
'bg-stone-200 text-stone-800',
'bg-stone-300 text-stone-800',
'bg-stone-400 text-stone-800',
];
return shades[shadeIndex];
};
const SpeciesComponent: React.FC<SpeciesProps> = ({
datasetId
}) => {
const { isAuthenticated, isLoading: authLoading, getAccessToken } = useKindeAuth();
const [species, setSpecies] = useState<Species[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
// Reset state when dataset changes
setSpecies([]);
setLoading(true);
setError(null);
const fetchSpecies = async () => {
if (!isAuthenticated || !datasetId) {
if (!authLoading) {
setLoading(false);
}
return;
}
try {
const accessToken = await getAccessToken();
// Fetch species for the selected dataset
const url = `/api/species?datasetId=${encodeURIComponent(datasetId)}`;
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
});
if (!response.ok) {
throw new Error(`Server error: ${response.status}`);
}
const data = await response.json() as SpeciesResponse;
if (!data.data || !Array.isArray(data.data)) {
throw new Error("Invalid response format");
}
setSpecies(data.data);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to fetch species";
setError(errorMessage);
} finally {
setLoading(false);
}
};
if (isAuthenticated && !authLoading) {
fetchSpecies();
}
}, [isAuthenticated, authLoading, getAccessToken, datasetId]);
return (
<div className="card p-6 bg-white shadow-sm rounded-lg">
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-bold">
<span className="text-gray-800">Species</span>
</h2>
</div>
{loading && <div className="py-4 text-center text-gray-500">Loading species...</div>}
{error && (
<div className="p-4 bg-red-50 text-red-700 rounded-md mb-4">
<p className="font-medium">Error loading species</p>
<p className="text-sm mt-1">{error}</p>
</div>
)}
{!loading && !error && species.length > 0 && (
<div className="w-full overflow-visible">
<Table>
<TableHeader className="bg-muted">
<TableRow className="border-b-2 border-primary/20">
<TableHead className="w-[240px] py-3 font-bold text-sm uppercase">Label</TableHead>
<TableHead className="py-3 font-bold text-sm uppercase">eBird Code</TableHead>
<TableHead className="py-3 font-bold text-sm uppercase">Description</TableHead>
<TableHead className="py-3 font-bold text-sm uppercase">Call Types</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{species.map((speciesItem) => (
<TableRow key={speciesItem.id}>
<TableCell className="font-medium whitespace-normal break-words">{speciesItem.label}</TableCell>
<TableCell className="whitespace-normal break-words">{speciesItem.ebirdCode || "—"}</TableCell>
<TableCell className="whitespace-normal break-words">{speciesItem.description || "—"}</TableCell>
<TableCell className="whitespace-normal break-words">
{speciesItem.callTypes.length > 0
? (
<div className="flex flex-wrap gap-1">
{speciesItem.callTypes.map(callType => (
<span
key={callType.id}
className={`inline-block px-2 py-1 rounded-full text-xs font-medium ${
getGrayShade(callType.label)
}`}
>
{callType.label}
</span>
))}
</div>
)
: "—"
}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{!loading && !error && species.length === 0 && (
<div className="p-4 bg-yellow-50 text-yellow-800 rounded-md">
<p className="font-medium">No species found for this dataset</p>
</div>
)}
</div>
);
};
export default SpeciesComponent;