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
29 changes: 11 additions & 18 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { useRXGroupsStore } from './store/rxGroupsStore';
import { useEncryptionKeysStore } from './store/encryptionKeysStore';
import { useRadioStore } from './store/radioStore';
import { useRadioConnection } from './hooks/useRadioConnection';
import { useAlert } from './hooks/useAlert';
import { importChannelsFromCSV, importContactsFromCSV } from './services/csv';
import type { CodeplugData } from './services/codeplugExport';
import { sampleChannels, sampleContacts, sampleZones } from './utils/sampleData';
Expand All @@ -36,8 +37,7 @@ import { useLogStore } from './store/logStore';
function App() {
const [activeTab, setActiveTab] = useState('channels');
const [showStartupModal, setShowStartupModal] = useState(true);
const [alertOpen, setAlertOpen] = useState(false);
const [alertMessage, setAlertMessage] = useState('');
const { alertOpen, alertMessage, alertTitle, showAlert, closeAlert } = useAlert('Import');
const { setChannels, channels } = useChannelsStore();
const { setContacts } = useContactsStore();
const { setZones } = useZonesStore();
Expand Down Expand Up @@ -192,11 +192,9 @@ function App() {
`• ${codeplugData.rxGroups?.length ?? 0} RX group(s)`,
`• ${codeplugData.encryptionKeys?.length ?? 0} encryption key(s)`,
].filter(Boolean);
setAlertMessage(`Successfully imported codeplug!\n\n${lines.join('\n')}`);
setAlertOpen(true);
showAlert(`Successfully imported codeplug!\n\n${lines.join('\n')}`);
} catch (error) {
setAlertMessage(`Failed to import codeplug: ${error instanceof Error ? error.message : 'Unknown error'}`);
setAlertOpen(true);
showAlert(`Failed to import codeplug: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
} else {
// Legacy CSV import support
Expand All @@ -207,26 +205,21 @@ function App() {
if (result.success && result.channels) {
setChannels(result.channels);
setShowStartupModal(false);
setAlertMessage(`Successfully imported ${result.channels.length} channels`);
setAlertOpen(true);
showAlert(`Successfully imported ${result.channels.length} channels`);
} else {
setAlertMessage(`Import failed: ${result.errors?.join(', ') || 'Unknown error'}`);
setAlertOpen(true);
showAlert(`Import failed: ${result.errors?.join(', ') || 'Unknown error'}`);
}
} else if (fileName.includes('contact')) {
const result = importContactsFromCSV(text);
if (result.success && result.contacts) {
setContacts(result.contacts);
setShowStartupModal(false);
setAlertMessage(`Successfully imported ${result.contacts.length} contacts`);
setAlertOpen(true);
showAlert(`Successfully imported ${result.contacts.length} contacts`);
} else {
setAlertMessage(`Import failed: ${result.errors?.join(', ') || 'Unknown error'}`);
setAlertOpen(true);
showAlert(`Import failed: ${result.errors?.join(', ') || 'Unknown error'}`);
}
} else {
setAlertMessage('File must be a codeplug (.neonplug) or CSV file containing "channel" or "contact" in the filename');
setAlertOpen(true);
showAlert('File must be a codeplug (.neonplug) or CSV file containing "channel" or "contact" in the filename');
}
}

Expand Down Expand Up @@ -307,8 +300,8 @@ function App() {
/>
<ConfirmModal
isOpen={alertOpen}
onClose={() => setAlertOpen(false)}
title="Import"
onClose={closeAlert}
title={alertTitle}
message={alertMessage}
confirmLabel="OK"
variant="alert"
Expand Down
5 changes: 3 additions & 2 deletions src/components/channels/ChannelsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useMemo, useCallback, useRef } from 'react';
import { formatPlural } from '../../utils/formatPlural';
import { useChannelsStore } from '../../store/channelsStore';
import { useRadioSettingsStore } from '../../store/radioSettingsStore';
import { useRadioCapabilities } from '../../hooks/useRadioCapabilities';
Expand Down Expand Up @@ -127,7 +128,7 @@ export const ChannelsTab: React.FC = () => {
<h2 className="text-2xl font-bold text-neon-cyan">Channels</h2>
<div className="flex items-center gap-4">
<div className="text-cool-gray">
{filteredChannels.length - vfoChannels.length} channel{(filteredChannels.length - vfoChannels.length) !== 1 ? 's' : ''} {vfoChannels.length > 0 && `(${vfoChannels.length} VFO${vfoChannels.length !== 1 ? 's' : ''})`}
{filteredChannels.length - vfoChannels.length} {formatPlural(filteredChannels.length - vfoChannels.length, 'channel')} {vfoChannels.length > 0 && `(${vfoChannels.length} ${formatPlural(vfoChannels.length, 'VFO')})`}
</div>
<button
onClick={handleAddChannel}
Expand Down Expand Up @@ -196,7 +197,7 @@ export const ChannelsTab: React.FC = () => {
onClose={() => setDeleteSelectedOpen(false)}
onConfirm={handleDeleteSelectedConfirm}
title="Delete channels"
message={`Delete ${pendingDeleteCount} selected channel${pendingDeleteCount !== 1 ? 's' : ''}?`}
message={`Delete ${pendingDeleteCount} selected ${formatPlural(pendingDeleteCount, 'channel')}?`}
confirmLabel="Delete"
variant="danger"
/>
Expand Down
11 changes: 6 additions & 5 deletions src/components/contacts/ContactsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect, useCallback, useMemo, startTransition } from 'react';
import { formatPlural } from '../../utils/formatPlural';
import { useContactsStore } from '../../store/contactsStore';
import { useRadioStore } from '../../store/radioStore';
import { useRadioConnection } from '../../hooks/useRadioConnection';
Expand Down Expand Up @@ -463,12 +464,12 @@ export const ContactsTab: React.FC = () => {
if (truncated) {
const removed = totalContacts - contactCapacity;
setTruncationWarning(
`Warning: ${removed.toLocaleString()} contact${removed === 1 ? '' : 's'} were removed due to limited space. ` +
`Warning: ${removed.toLocaleString()} ${formatPlural(removed, 'contact')} were removed due to limited space. ` +
`Your radio supports ${contactCapacity.toLocaleString()} contacts, but ${totalContacts.toLocaleString()} were downloaded.`
);
}

setProgressMessage(`Successfully downloaded ${contactsToSave.length.toLocaleString()} contact${contactsToSave.length === 1 ? '' : 's'} from ${countriesToFetch.length} countr${countriesToFetch.length === 1 ? 'y' : 'ies'}${selectedStates.length > 0 ? ` (${selectedStates.length} US state${selectedStates.length !== 1 ? 's' : ''})` : ''}`);
setProgressMessage(`Successfully downloaded ${contactsToSave.length.toLocaleString()} ${formatPlural(contactsToSave.length, 'contact')} from ${countriesToFetch.length} ${formatPlural(countriesToFetch.length, 'country', 'countries')}${selectedStates.length > 0 ? ` (${selectedStates.length} US ${formatPlural(selectedStates.length, 'state')})` : ''}`);
setProgress(100);

// Keep selection checked so user can download again if needed
Expand Down Expand Up @@ -642,7 +643,7 @@ export const ContactsTab: React.FC = () => {
</div>
{selectedStates.length > 0 && (
<p className="text-xs text-neon-cyan mt-2">
{selectedStates.length} state{selectedStates.length !== 1 ? 's' : ''} selected
{selectedStates.length} {formatPlural(selectedStates.length, 'state')} selected
</p>
)}
</div>
Expand Down Expand Up @@ -670,7 +671,7 @@ export const ContactsTab: React.FC = () => {

{selectedCountries.length > 0 && (
<span className="text-sm text-cool-gray">
{selectedCountries.length} countr{selectedCountries.length === 1 ? 'y' : 'ies'} selected
{selectedCountries.length} {formatPlural(selectedCountries.length, 'country', 'countries')} selected
</span>
)}
</div>
Expand Down Expand Up @@ -699,7 +700,7 @@ export const ContactsTab: React.FC = () => {
<div className="mb-4 flex items-center justify-between">
<h2 className="text-2xl font-bold text-neon-cyan">CSV Contacts</h2>
<div className="text-cool-gray">
{contacts.length} / {contactCapacity.toLocaleString()} contact{contacts.length !== 1 ? 's' : ''}
{contacts.length} / {contactCapacity.toLocaleString()} {formatPlural(contacts.length, 'contact')}
</div>
</div>
<div className="mb-4 text-cool-gray text-sm">
Expand Down
5 changes: 3 additions & 2 deletions src/components/contacts/ContactsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useMemo } from 'react';
import { formatPlural } from '../../utils/formatPlural';
import { useContactsStore } from '../../store/contactsStore';
import { EmptyState } from '../ui/EmptyState';
import { Card } from '../ui/Card';
Expand Down Expand Up @@ -75,7 +76,7 @@ export const ContactsTable: React.FC = () => {
</div>
{searchQuery && (
<div className="mt-2 text-xs text-cool-gray">
Found {filteredContacts.length.toLocaleString()} contact{filteredContacts.length !== 1 ? 's' : ''} matching "{searchQuery}"
Found {filteredContacts.length.toLocaleString()} {formatPlural(filteredContacts.length, 'contact')} matching "{searchQuery}"
</div>
)}
</div>
Expand Down Expand Up @@ -130,7 +131,7 @@ export const ContactsTable: React.FC = () => {
{filteredContacts.length > 0 && totalPages > 1 && (
<div className="mt-2 pt-2 border-t border-neon-cyan border-opacity-20 flex items-center justify-between text-sm text-cool-gray px-2 pb-2">
<span>
Showing {currentPage * CONTACTS_PER_PAGE + 1}-{Math.min((currentPage + 1) * CONTACTS_PER_PAGE, filteredContacts.length)} of {filteredContacts.length.toLocaleString()} contact{filteredContacts.length !== 1 ? 's' : ''}
Showing {currentPage * CONTACTS_PER_PAGE + 1}-{Math.min((currentPage + 1) * CONTACTS_PER_PAGE, filteredContacts.length)} of {filteredContacts.length.toLocaleString()} {formatPlural(filteredContacts.length, 'contact')}
{searchQuery && ` (${contacts.length.toLocaleString()} total)`}
</span>
<div className="flex items-center gap-2">
Expand Down
38 changes: 14 additions & 24 deletions src/components/diagnostics/DiagnosticsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState, useMemo, useRef, useEffect } from 'react';
import { useAlert } from '../../hooks/useAlert';
import { useRadioStore } from '../../store/radioStore';
import { useRadioCapabilities } from '../../hooks/useRadioCapabilities';
import { useRadioSettingsStore } from '../../store/radioSettingsStore';
Expand Down Expand Up @@ -79,8 +80,7 @@ export const DiagnosticsTab: React.FC = () => {
const [txContactLookupChannel, setTxContactLookupChannel] = useState<string>('');
const [showBootImageSection, setShowBootImageSection] = useState(true);
const [inspectBootImageOffset, setInspectBootImageOffset] = useState<string>('');
const [alertOpen, setAlertOpen] = useState(false);
const [alertMessage, setAlertMessage] = useState('');
const { alertOpen, alertMessage, alertTitle, showAlert, closeAlert } = useAlert();

const { logs, clearLogs, maxLogs, setMaxLogs } = useLogStore();

Expand Down Expand Up @@ -355,8 +355,7 @@ export const DiagnosticsTab: React.FC = () => {
}

if (blocksAdded === 0) {
setAlertMessage('No metadata blocks available to download. Please read from radio first.');
setAlertOpen(true);
showAlert('No metadata blocks available to download. Please read from radio first.');
return;
}

Expand All @@ -374,15 +373,13 @@ export const DiagnosticsTab: React.FC = () => {
URL.revokeObjectURL(url);
} catch (error) {
console.error('Error creating zip:', error);
setAlertMessage('Failed to create zip file. See console for details.');
setAlertOpen(true);
showAlert('Failed to create zip file. See console for details.');
}
};

const handleExportExpectedWriteHex = () => {
if (channels.length === 0) {
setAlertMessage('No channels available to export.');
setAlertOpen(true);
showAlert('No channels available to export.');
return;
}

Expand Down Expand Up @@ -422,8 +419,7 @@ export const DiagnosticsTab: React.FC = () => {

const handleExportExpectedWriteBin = () => {
if (channels.length === 0) {
setAlertMessage('No channels available to export.');
setAlertOpen(true);
showAlert('No channels available to export.');
return;
}

Expand Down Expand Up @@ -452,8 +448,7 @@ export const DiagnosticsTab: React.FC = () => {

const handleFullDebugExport = async () => {
if (channels.length === 0 && zones.length === 0 && logs.length === 0 && blockMetadata.size === 0 && blockData.size === 0) {
setAlertMessage('No data or logs to export. Please read from radio first.');
setAlertOpen(true);
showAlert('No data or logs to export. Please read from radio first.');
return;
}

Expand Down Expand Up @@ -570,15 +565,13 @@ export const DiagnosticsTab: React.FC = () => {
URL.revokeObjectURL(url);
} catch (error) {
console.error('Error creating export:', error);
setAlertMessage('Failed to create export. See console for details.');
setAlertOpen(true);
showAlert('Failed to create export. See console for details.');
}
};

const handleWriteBlocksExport = () => {
if (writeBlockData.size === 0) {
setAlertMessage('No write blocks available. Please write to radio first.');
setAlertOpen(true);
showAlert('No write blocks available. Please write to radio first.');
return;
}

Expand All @@ -589,8 +582,7 @@ export const DiagnosticsTab: React.FC = () => {

const handleMetadataAnalysisExport = () => {
if (blockMetadata.size === 0) {
setAlertMessage('No block metadata available. Please read from radio first.');
setAlertOpen(true);
showAlert('No block metadata available. Please read from radio first.');
return;
}

Expand Down Expand Up @@ -2195,8 +2187,7 @@ export const DiagnosticsTab: React.FC = () => {
URL.revokeObjectURL(url);
} catch (error) {
console.error('Error generating zip:', error);
setAlertMessage('Failed to generate zip file');
setAlertOpen(true);
showAlert('Failed to generate zip file');
}
}}
className="px-3 py-1 text-xs text-yellow-400 hover:text-yellow-300 border border-yellow-600/30 hover:border-yellow-400 rounded transition-colors"
Expand Down Expand Up @@ -3281,8 +3272,7 @@ export const DiagnosticsTab: React.FC = () => {
// Parse CPS CSV format
const lines = content.split('\n').filter(line => line.trim());
if (lines.length < 2) {
setAlertMessage('CSV must have at least a header row and one data row');
setAlertOpen(true);
showAlert('CSV must have at least a header row and one data row');
return;
}

Expand Down Expand Up @@ -3822,8 +3812,8 @@ export const DiagnosticsTab: React.FC = () => {
</div>
<ConfirmModal
isOpen={alertOpen}
onClose={() => setAlertOpen(false)}
title="Notice"
onClose={closeAlert}
title={alertTitle}
message={alertMessage}
confirmLabel="OK"
variant="alert"
Expand Down
20 changes: 8 additions & 12 deletions src/components/digital/DigitalTab.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useEffect, useMemo, useState } from 'react';
import { useAlert } from '../../hooks/useAlert';
import { useRadioStore } from '../../store/radioStore';
import { useRadioCapabilities } from '../../hooks/useRadioCapabilities';
import { useEncryptionKeysStore } from '../../store/encryptionKeysStore';
Expand Down Expand Up @@ -111,8 +112,7 @@ export const DigitalTab: React.FC = () => {

const handleAddContact = () => {
if (quickContacts.length >= talkGroupsMax) {
setAlertMessage(`Maximum of ${talkGroupsMax} talk groups allowed.`);
setAlertOpen(true);
showAlert(`Maximum of ${talkGroupsMax} talk groups allowed.`);
return;
}
addContact({
Expand All @@ -123,8 +123,7 @@ export const DigitalTab: React.FC = () => {
});
};

const [alertOpen, setAlertOpen] = useState(false);
const [alertMessage, setAlertMessage] = useState('');
const { alertOpen, alertMessage, alertTitle, showAlert, closeAlert } = useAlert();
const [deleteConfirm, setDeleteConfirm] = useState<
{ type: 'contact'; index: number } | { type: 'message'; index: number } | { type: 'radioId'; index: number } | null
>(null);
Expand All @@ -135,8 +134,7 @@ export const DigitalTab: React.FC = () => {

const handleAddMessage = () => {
if (messages.length >= 20) {
setAlertMessage('Maximum of 20 quick messages allowed.');
setAlertOpen(true);
showAlert('Maximum of 20 quick messages allowed.');
return;
}
const newIndex = messages.length;
Expand All @@ -154,8 +152,7 @@ export const DigitalTab: React.FC = () => {

const handleAddRadioId = () => {
if (radioIds.length >= dmrRadioIdsMax) {
setAlertMessage(`Maximum of ${dmrRadioIdsMax} DMR Radio IDs allowed.`);
setAlertOpen(true);
showAlert(`Maximum of ${dmrRadioIdsMax} DMR Radio IDs allowed.`);
return;
}
const newIndex = radioIds.length;
Expand Down Expand Up @@ -273,8 +270,7 @@ export const DigitalTab: React.FC = () => {
const dmrIdValue = parseInt(e.target.value, 10);
if (isNaN(dmrIdValue) || dmrIdValue < 0 || dmrIdValue > 0xFFFFFF) return;
if (dmrIdValue > 0 && !isValidDMRId(dmrIdValue)) {
setAlertMessage('DMR ID must be between 1 and 9,999,999 (0 = none).');
setAlertOpen(true);
showAlert('DMR ID must be between 1 and 9,999,999 (0 = none).');
return;
}
updateRadioId(radioId.index, {
Expand Down Expand Up @@ -813,8 +809,8 @@ export const DigitalTab: React.FC = () => {
/>
<ConfirmModal
isOpen={alertOpen}
onClose={() => setAlertOpen(false)}
title="Notice"
onClose={closeAlert}
title={alertTitle}
message={alertMessage}
confirmLabel="OK"
variant="alert"
Expand Down
5 changes: 3 additions & 2 deletions src/components/import/sources/AirportSource.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { formatPlural } from '../../../utils/formatPlural';
import { useImportStores } from '../../../hooks/useImportStores';
import { getNextChannelNumber, selectionCardClass } from '../../../utils/importHelpers';
import { generateAirportChannels } from '../../../services/airportChannels';
Expand Down Expand Up @@ -108,7 +109,7 @@ export const AirportSource: React.FC<AirportSourceProps> = ({
<>
<div className="flex justify-between items-center mb-4">
<SectionTitle as="h4" size="md">
Found {airports.length} Airport{airports.length !== 1 ? 's' : ''}
Found {airports.length} {formatPlural(airports.length, 'Airport')}
</SectionTitle>
<SelectAllButtons onSelectAll={handleSelectAllAirports} onDeselectAll={handleDeselectAllAirports} />
</div>
Expand Down Expand Up @@ -195,7 +196,7 @@ export const AirportSource: React.FC<AirportSourceProps> = ({
>
{isAddingAirports
? 'Adding Airport Channels...'
: `Add ${selectedAirports.size} Airport Channel${selectedAirports.size !== 1 ? 's' : ''}`}
: `Add ${selectedAirports.size} ${formatPlural(selectedAirports.size, 'Airport Channel')}`}
</Button>
</>
)}
Expand Down
Loading
Loading