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
19 changes: 4 additions & 15 deletions src/app/new-contact/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { frequencyToBand, AMATEUR_BANDS } from '@/lib/bands';
import { AMATEUR_MODES, defaultRstForMode } from '@/lib/modes';
import { gridToLatLon, distanceKm, bearingDeg, compassPoint, kmToMiles } from '@/lib/grid';

void _PageHeader;
Expand All @@ -61,7 +62,7 @@ interface PreviousContact {
notes?: string;
}

const MODES = ['SSB', 'CW', 'FT8', 'FT4', 'RTTY', 'PSK31', 'AM', 'FM'] as const;
const MODES = AMATEUR_MODES;
const BAND_PILLS = AMATEUR_BANDS;

// QRZ XML license-class codes — used to give the chip a human-readable label.
Expand Down Expand Up @@ -381,20 +382,8 @@ export default function NewContactPage() {
};

const handleSelectMode = (value: string) => {
setFormData((prev) => {
const next = { ...prev, mode: value };
if (value === 'CW') {
next.rst_sent = '599';
next.rst_received = '599';
} else if (['SSB', 'FM', 'AM'].includes(value)) {
next.rst_sent = '59';
next.rst_received = '59';
} else if (['FT8', 'FT4', 'PSK31', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA'].includes(value)) {
next.rst_sent = '-10';
next.rst_received = '-10';
}
return next;
});
const rst = defaultRstForMode(value);
setFormData((prev) => ({ ...prev, mode: value, rst_sent: rst, rst_received: rst }));
};

const handleSelectBand = (value: string) => {
Expand Down
9 changes: 2 additions & 7 deletions src/components/QuickLogCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from '@/components/ui/select';
import { cn } from '@/lib/utils';
import { frequencyToBand, AMATEUR_BANDS } from '@/lib/bands';
import { AMATEUR_MODES, defaultRstForMode } from '@/lib/modes';

interface Station {
id: number;
Expand All @@ -39,15 +40,9 @@ interface LookupResult {
error?: string;
}

const MODES = ['SSB', 'CW', 'FT8', 'FT4', 'RTTY', 'PSK31', 'AM', 'FM'] as const;
const MODES = AMATEUR_MODES;
const BANDS = AMATEUR_BANDS;

function defaultRstForMode(mode: string): string {
if (mode === 'CW') return '599';
if (['FT8', 'FT4', 'PSK31', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA'].includes(mode)) return '-10';
return '59';
}

function formatUtcClock(d: Date): string {
const hh = String(d.getUTCHours()).padStart(2, '0');
const mm = String(d.getUTCMinutes()).padStart(2, '0');
Expand Down
24 changes: 24 additions & 0 deletions src/lib/modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,27 @@ export const AMATEUR_MODES = [
] as const;

export type AmateurMode = (typeof AMATEUR_MODES)[number];

// Modes whose signal report is conventionally a dB SNR (e.g. "-10") rather than
// an RST. Both logging forms carried their own copy of this list; keeping it
// here as the single source of truth stops the two from drifting apart (and from
// falling behind AMATEUR_MODES) as new digital modes become loggable.
//
// The WSJT-X / weak-signal family (FT8, FT4, JS8, FST4, JT65, JT9, Q65, MSK144)
// genuinely reports dB. The keyboard PSK/RTTY/MFSK/Olivia/Contestia group is
// grouped here to preserve the default Nextlog has always applied to those menu
// entries — not because RTTY is a dB mode.
const DB_REPORT_MODES = new Set<string>([
'FT8', 'FT4', 'JS8', 'FST4', 'JT65', 'JT9', 'Q65', 'MSK144',
'PSK31', 'PSK63', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA',
]);

// The signal report a logging form should pre-fill when an operator picks `mode`.
// CW takes RST with a tone digit (599); dB-report digital modes take -10; every
// other mode (phone, digital voice, image, packet) takes a 59 RS(T).
export function defaultRstForMode(mode: string): string {
const m = mode.trim().toUpperCase();
if (m === 'CW') return '599';
if (DB_REPORT_MODES.has(m)) return '-10';
return '59';
}
53 changes: 52 additions & 1 deletion tests/modes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { test, expect } from '@playwright/test';
import { AMATEUR_MODES } from '@/lib/modes';
import { AMATEUR_MODES, defaultRstForMode } from '@/lib/modes';

// The mode list is the canonical set of operating modes Nextlog can store, and
// the single source of truth the contact-search mode dropdown draws from. These
Expand Down Expand Up @@ -54,3 +54,54 @@ test.describe('modes module', () => {
expect(AMATEUR_MODES.slice(0, 4)).toEqual(['SSB', 'CW', 'FT8', 'FT4']);
});
});

test.describe('defaultRstForMode', () => {
test('CW gets a 599 signal report', () => {
expect(defaultRstForMode('CW')).toBe('599');
});

test('phone modes get a 59 signal report', () => {
for (const mode of ['SSB', 'AM', 'FM']) {
expect(defaultRstForMode(mode)).toBe('59');
}
});

test('digital dB-report modes default to -10', () => {
// WSJT-X / weak-signal families report a dB SNR, and Nextlog has always
// defaulted its PSK/RTTY/MFSK/Olivia/Contestia menu entries the same way.
const dbModes = [
'FT8', 'FT4', 'JS8', 'FST4', 'JT65', 'JT9', 'Q65', 'MSK144',
'PSK31', 'PSK63', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA',
];
for (const mode of dbModes) {
expect(defaultRstForMode(mode)).toBe('-10');
}
});

test('preserves the exact classification the logging forms used before centralizing', () => {
// The two logging forms each carried their own copy of this heuristic; the
// shared helper must not change the default for any mode they both handled.
expect(defaultRstForMode('CW')).toBe('599');
for (const mode of ['FT8', 'FT4', 'PSK31', 'RTTY', 'MFSK', 'OLIVIA', 'CONTESTIA']) {
expect(defaultRstForMode(mode)).toBe('-10');
}
});

test('digital-voice and image modes fall back to 59', () => {
for (const mode of ['DMR', 'DSTAR', 'C4FM', 'YSF', 'M17', 'FREEDV', 'SSTV', 'PACKET', 'ATV']) {
expect(defaultRstForMode(mode)).toBe('59');
}
});

test('is case-insensitive', () => {
expect(defaultRstForMode('cw')).toBe('599');
expect(defaultRstForMode('ft8')).toBe('-10');
expect(defaultRstForMode('ssb')).toBe('59');
});

test('returns a default for every canonical mode', () => {
for (const mode of AMATEUR_MODES) {
expect(['59', '599', '-10']).toContain(defaultRstForMode(mode));
}
});
});
Loading