4FBIL6IZUDNCXTM6EUHTEOJRHVI4LIIX4BU2IXPXKR362GKIAJMQC
XEXJLBOH6HQAUZRUNH3CCPNUD4HRNCKMRZ5UJ6UUCO76KV6WUJAAC
PVQBFR72OCQGYF2G2KDWNKBHWJ24N6D653X6KARBGUSYBIHIXPRQC
YX7LU4WRAUDMWS3DEDXZDSF6DXBHLYDWVSMSRK6KIW3MO6GRXSVQC
M3JUJ2WWZGCVMBITKRM5FUJMHFYL2QRMXJUVRUE4AC2RF74AOL5AC
2OYSY7VNMQ4C3CQMX7VKKD2JSZM72TJ3LL4GEDUQYG6RJTNIROLAC
O7W4FZVRKDQDAAXEW4T7P262PPRILRCSSACODMUTQZ6VNR36PVCQC
M4UG5FMI5ICQRLJCCNSLV3ZKCGIXDQ65ECJM4PBE3NZYHT3CJLDAC
DOQBQX4IQSDBYYSBP4BEMTMJUKZPSC33KKXPAGZ3A5BRJMMKHCRQC
HM75N4NTZ4BBSSDC7TUSYOQ4SIF3G6KPZA5QRYCVCVRSKQVTJAXAC
4M3EBLTLSS2BRCM42ZP7WVD4YMRRLGV2P2XF47IAV5XHHJD52HTQC
CVW5G63BAFGQBR5YTZIHTZVVVECP2U3XI76APTKKOODGWHRBEIBQC
RLH37YB4D7O42IFM2T7GJG4AVVAURWBZ7AOTHAWR7YJZRG3JOPLQC
ROQGXQWL2V363K3W7TVVYKIAX4N4IWRERN5BJ7NYJRRVB6OMIJ4QC
4RBE543WLHA7PIYT4W7YEJPF6XKZ2UGKPJBQ3CTLJ44AOMGHCEYQC
LYPSC7BOH6T45FCPRHSCXILAJSJ74D5WSQTUIKPWD5ECXOYGUY5AC
J2RLNDEXTGAV4BB6ANIIR7XJLJBHSB4NFQWSBWHNAFB6DMLGS5RAC
HBM7XFBGVMKW3P3VZFNQTJMINZ4DC3D4TZMT4TPTJRXC62HKZMMQC
POIBWSL3JFHT2KN3STFSJX3INSYKEJTX6KSW3N7BVEKWX2GJ6T7QC
/**
* UI utility functions
*/
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/**
* Combines class names with Tailwind's merge function
* This utility is used by shadcn components for conditional class handling
*/
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/**
* Re-export all utility functions from a single entry point
*/
export * from './ui';
export * from './formatting';
export * from './data-processing';
/**
* Convert seconds to a formatted duration string (MM:SS)
*
* @param durationSec - Duration in seconds
* @returns Formatted duration string in MM:SS format
*/
export function formatDuration(durationSec: number): string {
const minutes = Math.floor(durationSec / 60);
const seconds = Math.floor(durationSec % 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
/**
* Format moon phase value as a fixed decimal string
*
* @param phase - Moon phase value (0-1) or null/undefined
* @returns Formatted moon phase string with 2 decimal places or "—" if not available
*/
export function formatMoonPhase(phase: number | null | undefined): string {
if (phase === null || phase === undefined) return "—";
try {
return Number(phase).toFixed(2);
} catch {
return "—";
}
}
/**
* Format battery voltage with units
*
* @param volts - Battery voltage value or null/undefined
* @returns Formatted voltage string with units or "—" if not available
*/
export function formatBatteryVoltage(volts: number | null | undefined): string {
if (volts === null || volts === undefined) return "—";
try {
return Number(volts).toFixed(1) + 'V';
} catch {
return "—";
}
}
/**
* Format temperature with units
*
* @param temp - Temperature in Celsius or null/undefined
* @returns Formatted temperature string with units or "—" if not available
*/
export function formatTemperature(temp: number | null | undefined): string {
if (temp === null || temp === undefined) return "—";
try {
return Number(temp).toFixed(1) + '°C';
} catch {
return "—";
}
}
/**
* Format date and time in a human-readable format
*
* @param isoString - ISO date string
* @returns Formatted date and time string
*/
export function formatDateTime(isoString: string): string {
try {
const date = new Date(isoString);
return new Intl.DateTimeFormat('en-GB', {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
hour12: false
}).format(date);
} catch {
return isoString;
}
}
/**
* Format a sample rate value with kHz unit
*
* @param rate - Sample rate in Hz
* @returns Formatted sample rate with kHz unit
*/
export function formatSampleRate(rate: number): string {
return `${(rate / 1000).toFixed(1)} kHz`;
}
/**
* Process JSON metadata string to handle various formats and edge cases
*
* @param jsonData - Metadata in string or object format
* @returns Properly processed metadata object or the original if parsing fails
*/
export function processJsonMetadata(jsonData: unknown): unknown {
if (!jsonData) return null;
let processedJson = jsonData;
try {
// If it's already a string that looks like a JSON string, try to parse it
if (typeof jsonData === 'string' &&
(jsonData.startsWith('{') || jsonData.startsWith('['))) {
processedJson = JSON.parse(jsonData);
}
// Some databases might return the json as a string with escaped quotes
else if (typeof jsonData === 'string' && jsonData.includes('\\"')) {
// Handle double-escaped JSON
const unescaped = jsonData.replace(/\\"/g, '"');
processedJson = JSON.parse(unescaped);
}
} catch (e) {
console.error("Error processing metadata JSON:", e);
// Keep the original if parsing fails
processedJson = jsonData;
}
return processedJson;
}
/**
* Create pagination metadata for API responses
*
* @param currentPage - Current page number
* @param pageSize - Items per page
* @param totalItems - Total number of items
* @returns Pagination metadata object
*/
export function createPaginationMetadata(currentPage: number, pageSize: number, totalItems: number) {
const totalPages = Math.ceil(totalItems / pageSize);
return {
currentPage,
pageSize,
totalPages,
totalItems,
hasNextPage: currentPage < totalPages,
hasPreviousPage: currentPage > 1,
};
}
/**
* Check if valid metadata is present in any file
*
* @param files - Array of files to check
* @returns Boolean indicating if any file has valid metadata
*/
export function hasValidMetadata(files: Array<{ metadata: unknown }>): boolean {
return files.some(file => {
if (!file.metadata) return false;
try {
const meta = typeof file.metadata === 'string'
? JSON.parse(file.metadata)
: file.metadata;
return meta && typeof meta === 'object' && Object.keys(meta).length > 0;
} catch {
return false;
}
});
}
/**
* Check if any files have moth metadata
*
* @param files - Array of files to check
* @returns Boolean indicating if any file has moth metadata
*/
export function hasValidMothMetadata(files: Array<{ mothMetadata?: { gain: string | null; batteryV: number | null; tempC: number | null; } | null }>): boolean {
return files.some(file =>
file.mothMetadata &&
(file.mothMetadata.gain !== null ||
file.mothMetadata.batteryV !== null ||
file.mothMetadata.tempC !== null)
);
}
/**
* Check if any files have species information
*
* @param files - Array of files to check
* @returns Boolean indicating if any file has species data
*/
export function hasSpeciesData(files: Array<{ species?: Array<unknown> }>): boolean {
return files.some(file => file.species && file.species.length > 0);
}
/**
* Species-related types
*/
export interface CallType {
id: string;
label: string;
}
export interface Species {
id: string;
label: string;
ebirdCode: string | null;
description: string | null;
callTypes: CallType[];
}
export interface SpeciesResponse {
data: Species[];
}
/**
* Shared pagination types used across components
*/
export interface PaginationMetadata {
currentPage: number;
pageSize: number;
totalPages: number;
totalItems: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
}
/**
* Location-related types
*/
import { PaginationMetadata } from './pagination';
export interface Location {
id: string;
datasetId: string;
name: string;
latitude: number | null;
longitude: number | null;
description: string | null;
createdBy: string;
createdAt: string;
lastModified: string;
modifiedBy: string;
active: boolean;
}
export interface LocationsResponse {
data: Location[];
pagination?: PaginationMetadata;
}
/**
* Re-export all types from a single entry point
*/
export * from './pagination';
export * from './file';
export * from './dataset';
export * from './location';
export * from './cluster';
export type { CallType, Species, SpeciesResponse } from './species';
/**
* Common file-related types used across components
*/
import { PaginationMetadata } from './pagination';
export interface FileMetadata {
noiseType?: string[];
noiseLevel?: string;
[key: string]: unknown;
}
export interface MothMetadata {
gain: string | null;
batteryV: number | null;
tempC: number | null;
}
export interface FileSpecies {
id: string;
label: string;
ebirdCode: string | null;
description: string | null;
}
export interface File {
id: string;
fileName: string;
path: string | null;
timestampLocal: string;
duration: number;
sampleRate: number;
locationId: string;
clusterId?: string;
description: string | null;
maybeSolarNight: boolean | null;
maybeCivilNight: boolean | null;
moonPhase: number | null;
metadata: FileMetadata | null;
mothMetadata?: MothMetadata | null;
species?: FileSpecies[];
}
export interface FilesFilters {
solarNight: boolean | null;
civilNight: boolean | null;
speciesId: string | null;
datasetId?: string;
}
export interface FilesResponse {
data: File[];
pagination: PaginationMetadata;
filters?: FilesFilters;
}
export type NightFilter = 'none' | 'solarNight' | 'solarDay' | 'civilNight' | 'civilDay';
export interface SpeciesOption {
id: string;
label: string;
}
/**
* Dataset-related interfaces
*/
export interface Dataset {
id: string;
name: string;
public: boolean;
description?: string;
createdBy: string;
createdAt: string;
lastModified: string;
modifiedBy: string;
owner: string;
active: boolean;
type: string;
}
export interface DatasetsResponse {
data: Dataset[];
}
/**
* Cluster-related types
*/
export interface RecordingPattern {
recordS: number;
sleepS: number;
}
export interface Cluster {
id: string;
locationId: string;
name: string;
description: string | null;
createdBy: string;
createdAt: string;
lastModified: string;
modifiedBy: string;
active: boolean;
timezoneId: string | null;
cyclicRecordingPatternId: string | null;
recordingPattern: RecordingPattern | null;
sampleRate: number;
}
export interface ClustersResponse {
data: Cluster[];
}
interface FileMetadata {
noiseType?: string[];
noiseLevel?: string;
[key: string]: unknown;
}
interface MothMetadata {
gain: string | null;
batteryV: number | null;
tempC: number | null;
}
interface Species {
id: string;
label: string;
ebirdCode: string | null;
description: string | null;
}
interface File {
id: string;
fileName: string;
path: string | null;
timestampLocal: string;
duration: number;
sampleRate: number;
locationId: string;
clusterId: string;
description: string | null;
maybeSolarNight: boolean | null;
maybeCivilNight: boolean | null;
moonPhase: number | null;
metadata: FileMetadata | null;
mothMetadata?: MothMetadata | null;
species?: Species[];
}
import {
File,
FilesResponse,
NightFilter,
PaginationMetadata,
SpeciesOption
} from "../types";
interface PaginationMetadata {
currentPage: number;
pageSize: number;
totalPages: number;
totalItems: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
}
interface FilesFilters {
datasetId: string;
speciesId: string;
solarNight: boolean | null;
civilNight: boolean | null;
}
interface FilesResponse {
data: File[];
pagination: PaginationMetadata;
filters?: FilesFilters;
}
type NightFilter = 'none' | 'solarNight' | 'solarDay' | 'civilNight' | 'civilDay';
// Species data type for the dropdown
interface SpeciesOption {
id: string;
label: string;
}
// interface Dataset {
// id: string;
// name: string;
// }
interface LocationsResponse {
data: Location[];
pagination?: {
currentPage: number;
pageSize: number;
totalPages: number;
totalItems: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
};
}
// Removing unused interface
interface FileMetadata {
noiseType?: string[];
noiseLevel?: string;
[key: string]: unknown;
}
interface MothMetadata {
gain: string | null;
batteryV: number | null;
tempC: number | null;
}
interface Species {
id: string;
label: string;
ebirdCode: string | null;
description: string | null;
}
interface File {
id: string;
fileName: string;
path: string | null;
timestampLocal: string;
duration: number;
sampleRate: number;
locationId: string;
description: string | null;
maybeSolarNight: boolean | null;
maybeCivilNight: boolean | null;
moonPhase: number | null;
metadata: FileMetadata | null;
mothMetadata?: MothMetadata | null;
species?: Species[];
}
interface PaginationMetadata {
currentPage: number;
pageSize: number;
totalPages: number;
totalItems: number;
hasNextPage: boolean;
hasPreviousPage: boolean;
}
interface FilesFilters {
solarNight: boolean | null;
civilNight: boolean | null;
speciesId: string | null;
}
interface FilesResponse {
data: File[];
pagination: PaginationMetadata;
filters?: FilesFilters;
}
type NightFilter = 'none' | 'solarNight' | 'solarDay' | 'civilNight' | 'civilDay';
// Species data type for the dropdown
interface SpeciesOption {
id: string;
label: string;
}
import {
File,
FilesResponse,
NightFilter,
PaginationMetadata,
SpeciesOption
} from "../types";
interface RecordingPattern {
recordS: number;
sleepS: number;
}
interface Cluster {
id: string;
locationId: string;
name: string;
description: string | null;
createdBy: string;
createdAt: string;
lastModified: string;
modifiedBy: string;
active: boolean;
timezoneId: string | null;
cyclicRecordingPatternId: string | null;
recordingPattern: RecordingPattern | null;
sampleRate: number;
}
// Removed unused Location interface
import { Cluster, ClustersResponse } from "../types";