mirror of
https://github.com/jakobkordez/call-tester.git
synced 2025-05-15 16:20:29 +00:00
WIP
This commit is contained in:
parent
933deccca5
commit
0f06cb8b83
@ -3,6 +3,6 @@
|
||||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -1,3 +1,4 @@
|
||||
{
|
||||
"vite.autoStart": false,
|
||||
"cSpell.words": ["callsign", "dxcc", "adif", "graphviz", "clublog"]
|
||||
}
|
||||
|
42
README.md
42
README.md
@ -1,38 +1,18 @@
|
||||
# create-svelte
|
||||
# Callsign Tester
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/main/packages/create-svelte).
|
||||
An app for checking the format of a callsign and finding the country it belongs to.
|
||||
|
||||
## Creating a project
|
||||
## Obtaining the Clublog prefix database
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
Read how to obtain the Clublog prefix file [here](https://clublog.freshdesk.com/support/solutions/articles/54902-downloading-the-prefixes-and-exceptions-as-xml)
|
||||
|
||||
## Deploying the app
|
||||
|
||||
The app can be deployed using the following command:
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npm create svelte@latest
|
||||
|
||||
# create a new project in my-app
|
||||
npm create svelte@latest my-app
|
||||
yarn install
|
||||
yarn build
|
||||
```
|
||||
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To create a production version of your app:
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
|
||||
Deploy the contents of the `build` directory to your server.
|
||||
|
@ -14,6 +14,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.2",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@types/eslint": "^8.56.7",
|
||||
@ -25,8 +26,9 @@
|
||||
"eslint-plugin-svelte": "^2.36.0",
|
||||
"globals": "^15.0.0",
|
||||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tailwindcss": "^3.4.4",
|
||||
|
@ -1,6 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
|
@ -1,14 +1,16 @@
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const filePath = process.argv[2];
|
||||
if (!filePath) {
|
||||
console.error('Please provide a file path as the first argument');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const outPath = process.argv[3];
|
||||
if (!outPath) {
|
||||
console.error('Please provide an output path as the second argument');
|
||||
process.exit(1);
|
||||
}
|
||||
const OUT_DIR = '../src/assets/';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const outDir = path.resolve(__dirname, OUT_DIR);
|
||||
console.log('Outputting to', outDir);
|
||||
|
||||
import fs from 'fs';
|
||||
import { parseStringPromise } from 'xml2js';
|
||||
@ -21,16 +23,6 @@ const doc: IClublogFile = await parseStringPromise(file);
|
||||
|
||||
const now = new Date();
|
||||
|
||||
const entities: Map<number, string> = new Map();
|
||||
|
||||
for (const entity of doc.clublog.entities[0].entity) {
|
||||
const id = parseInt(entity.adif[0]);
|
||||
const name = capitalize(entity.name[0]);
|
||||
const end = entity.end?.[0];
|
||||
if (end && new Date(end) < now) continue;
|
||||
entities.set(id, name);
|
||||
}
|
||||
|
||||
const prefixes: { call: string; entity: number }[] = [];
|
||||
|
||||
for (const prefix of doc.clublog.prefixes[0].prefix) {
|
||||
@ -41,6 +33,7 @@ for (const prefix of doc.clublog.prefixes[0].prefix) {
|
||||
entity: parseInt(prefix.adif[0])
|
||||
});
|
||||
}
|
||||
console.log('Parsed', prefixes.length, 'prefixes');
|
||||
|
||||
// Build the initial trie
|
||||
import { TrieNode } from '../src/lib/models/trie';
|
||||
@ -52,13 +45,17 @@ for (const { call, entity } of prefixes) {
|
||||
|
||||
// Merge as many nodes as possible
|
||||
const nodes = new Map([...root.getAllNodes()].map((node) => [node.id, node]));
|
||||
console.log('Starting merge with', nodes.size, 'nodes');
|
||||
|
||||
// Bad merge algorithm, but it works
|
||||
let anyChanged = true;
|
||||
while (anyChanged) {
|
||||
anyChanged = false;
|
||||
for (const a of nodes.values()) {
|
||||
if (!nodes.has(a.id)) continue;
|
||||
for (const b of nodes.values()) {
|
||||
if (a === b) continue;
|
||||
if (!nodes.has(b.id)) continue;
|
||||
if (a.canMerge(b)) {
|
||||
for (const node of nodes.values()) {
|
||||
for (const [k, v] of node.children) {
|
||||
@ -69,13 +66,48 @@ while (anyChanged) {
|
||||
}
|
||||
nodes.delete(b.id);
|
||||
anyChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (anyChanged) break;
|
||||
}
|
||||
}
|
||||
console.log('Finished merge with', nodes.size, 'nodes');
|
||||
|
||||
// Validate the trie
|
||||
for (const { call, entity } of prefixes) {
|
||||
if (root.findRaw(call)?.entity !== entity) {
|
||||
console.error('Failed to find', call, entity);
|
||||
}
|
||||
}
|
||||
|
||||
// Minimize node IDs
|
||||
let i = 0;
|
||||
for (const node of root.getAllNodes()) {
|
||||
node.id = i++;
|
||||
}
|
||||
|
||||
// Output the trie
|
||||
const out = [...root.getAllNodes()].map((n) => n.encodeToString()).join('\n');
|
||||
fs.writeFileSync(outPath, out);
|
||||
const out = root.encodeToString();
|
||||
fs.writeFileSync(path.join(outDir, 'dxcc-tree.txt'), out);
|
||||
|
||||
// Format entities
|
||||
import { DxccEntity } from '../src/lib/models/dxcc-entity';
|
||||
|
||||
const entities: DxccEntity[] = [];
|
||||
|
||||
for (const entity of doc.clublog.entities[0].entity) {
|
||||
const id = parseInt(entity.adif[0]);
|
||||
const name = capitalize(entity.name[0]);
|
||||
const cqz = entity.cqz?.[0];
|
||||
const cont = entity.cont?.[0];
|
||||
const end = entity.end?.[0];
|
||||
if (end && new Date(end) < now) continue;
|
||||
entities.push({
|
||||
entity: id,
|
||||
name,
|
||||
cqz: cqz ? parseInt(cqz) : undefined,
|
||||
cont: cont ? cont : undefined
|
||||
});
|
||||
}
|
||||
|
||||
// Output the entities
|
||||
fs.writeFileSync(path.join(outDir, 'dxcc-entities.json'), JSON.stringify(entities, null, '\t'));
|
||||
|
@ -1,7 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.btn {
|
||||
@apply bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600 active:bg-blue-700;
|
||||
}
|
||||
|
2042
src/assets/dxcc-entities.json
Normal file
2042
src/assets/dxcc-entities.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@
|
||||
let selectionStart: number | null = inputText.length;
|
||||
let selectionEnd: number | null = inputText.length;
|
||||
|
||||
export const generateStyledText: (text: string) => string = (text: string) => text;
|
||||
export let generateStyledText: (text: string) => string = (text: string) => text;
|
||||
|
||||
$: styledText = generateStyledText(inputText) || '​';
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
t.setSelectionRange(selectionStart, selectionEnd);
|
||||
}
|
||||
}}
|
||||
placeholder="Enter a callsign"
|
||||
placeholder="..."
|
||||
/>
|
||||
<div class="styled-text shared" contenteditable="false" bind:innerHTML={styledText} />
|
||||
</div>
|
||||
@ -49,17 +49,17 @@
|
||||
font-size: 32px;
|
||||
padding: 8px 12px;
|
||||
text-align: center;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.input {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: transparent;
|
||||
color: transparent;
|
||||
caret-color: black;
|
||||
caret-color: white;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
@ -68,6 +68,7 @@
|
||||
}
|
||||
|
||||
.styled-text {
|
||||
background: #424242;
|
||||
border: 1px solid transparent;
|
||||
white-space: pre;
|
||||
word-wrap: break-word;
|
||||
|
108
src/lib/callsign.test.ts
Normal file
108
src/lib/callsign.test.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { parseCallsign } from './callsign';
|
||||
|
||||
describe('parseCallsign', () => {
|
||||
test('S52KJ', () => {
|
||||
const data = parseCallsign('S52KJ');
|
||||
expect(data).not.toBe(null);
|
||||
expect(data?.secondaryPrefix).toBe(null);
|
||||
expect(data?.basePrefix).toBe('S5');
|
||||
expect(data?.baseSuffix).toBe('2KJ');
|
||||
expect(data?.base).toBe('S52KJ');
|
||||
expect(data?.secondarySuffix).toBe(null);
|
||||
expect(data?.suffixPartOf).toBe(null);
|
||||
expect(data?.baseDxcc).toBe(499);
|
||||
expect(data?.prefixDxcc).toBe(null);
|
||||
});
|
||||
|
||||
test('s52kj', () => {
|
||||
const data = parseCallsign('s52kj');
|
||||
expect(data).not.toBe(null);
|
||||
expect(data?.secondaryPrefix).toBe(null);
|
||||
expect(data?.basePrefix).toBe('S5');
|
||||
expect(data?.baseSuffix).toBe('2KJ');
|
||||
expect(data?.base).toBe('S52KJ');
|
||||
expect(data?.secondarySuffix).toBe(null);
|
||||
expect(data?.suffixPartOf).toBe(null);
|
||||
expect(data?.baseDxcc).toBe(499);
|
||||
expect(data?.prefixDxcc).toBe(null);
|
||||
});
|
||||
|
||||
test('S52KJ/P', () => {
|
||||
const data = parseCallsign('S52KJ/P');
|
||||
expect(data).not.toBe(null);
|
||||
expect(data?.secondaryPrefix).toBe(null);
|
||||
expect(data?.basePrefix).toBe('S5');
|
||||
expect(data?.baseSuffix).toBe('2KJ');
|
||||
expect(data?.base).toBe('S52KJ');
|
||||
expect(data?.secondarySuffix).toBe('P');
|
||||
expect(data?.suffixPartOf).toBe(null);
|
||||
expect(data?.baseDxcc).toBe(499);
|
||||
expect(data?.prefixDxcc).toBe(null);
|
||||
});
|
||||
|
||||
test('9A/S52KJ', () => {
|
||||
const data = parseCallsign('9A/S52KJ');
|
||||
expect(data).not.toBe(null);
|
||||
expect(data?.secondaryPrefix).toBe('9A');
|
||||
expect(data?.basePrefix).toBe('S5');
|
||||
expect(data?.baseSuffix).toBe('2KJ');
|
||||
expect(data?.base).toBe('S52KJ');
|
||||
expect(data?.secondarySuffix).toBe(null);
|
||||
expect(data?.suffixPartOf).toBe(null);
|
||||
expect(data?.baseDxcc).toBe(499);
|
||||
expect(data?.prefixDxcc).toBe(497);
|
||||
});
|
||||
|
||||
test('9A/S52KJ/P', () => {
|
||||
const data = parseCallsign('9A/S52KJ/P');
|
||||
expect(data).not.toBe(null);
|
||||
expect(data?.secondaryPrefix).toBe('9A');
|
||||
expect(data?.basePrefix).toBe('S5');
|
||||
expect(data?.baseSuffix).toBe('2KJ');
|
||||
expect(data?.base).toBe('S52KJ');
|
||||
expect(data?.secondarySuffix).toBe('P');
|
||||
expect(data?.suffixPartOf).toBe(null);
|
||||
expect(data?.baseDxcc).toBe(499);
|
||||
expect(data?.prefixDxcc).toBe(497);
|
||||
});
|
||||
|
||||
test('S52KJ/A', () => {
|
||||
const data = parseCallsign('S52KJ/A');
|
||||
expect(data).not.toBe(null);
|
||||
expect(data?.secondaryPrefix).toBe(null);
|
||||
expect(data?.basePrefix).toBe('S5');
|
||||
expect(data?.baseSuffix).toBe('2KJ');
|
||||
expect(data?.base).toBe('S52KJ');
|
||||
expect(data?.secondarySuffix).toBe('A');
|
||||
expect(data?.suffixPartOf).toBe(null);
|
||||
expect(data?.baseDxcc).toBe(499);
|
||||
expect(data?.prefixDxcc).toBe(null);
|
||||
});
|
||||
|
||||
test('SV1KJ/A', () => {
|
||||
const data = parseCallsign('SV1KJ/A');
|
||||
expect(data).not.toBe(null);
|
||||
expect(data?.secondaryPrefix).toBe(null);
|
||||
expect(data?.basePrefix).toBe('SV');
|
||||
expect(data?.baseSuffix).toBe('1KJ');
|
||||
expect(data?.base).toBe('SV1KJ');
|
||||
expect(data?.secondarySuffix).toBe('A');
|
||||
expect(data?.suffixPartOf).toBe('base');
|
||||
expect(data?.baseDxcc).toBe(180);
|
||||
expect(data?.prefixDxcc).toBe(null);
|
||||
});
|
||||
|
||||
test('SV/S52KJ/A', () => {
|
||||
const data = parseCallsign('SV/S52KJ/A');
|
||||
expect(data).not.toBe(null);
|
||||
expect(data?.secondaryPrefix).toBe('SV');
|
||||
expect(data?.basePrefix).toBe('S5');
|
||||
expect(data?.baseSuffix).toBe('2KJ');
|
||||
expect(data?.base).toBe('S52KJ');
|
||||
expect(data?.secondarySuffix).toBe('A');
|
||||
expect(data?.suffixPartOf).toBe('prefix');
|
||||
expect(data?.baseDxcc).toBe(499);
|
||||
expect(data?.prefixDxcc).toBe(180);
|
||||
});
|
||||
});
|
@ -1 +1,75 @@
|
||||
import { findDxcc } from './dxcc-util';
|
||||
|
||||
export const callsignPattern = /^([A-Z\d]+\/)?([A-Z\d]+\d+[A-Z]+)(\/[A-Z\d]+)?$/i;
|
||||
|
||||
type CallsignData = {
|
||||
secondaryPrefix: string | null;
|
||||
basePrefix: string | null;
|
||||
baseSuffix: string | null;
|
||||
base: string;
|
||||
secondarySuffix: string | null;
|
||||
suffixPartOf: 'base' | 'prefix' | null;
|
||||
suffixDescription?: string;
|
||||
baseDxcc: number | null;
|
||||
prefixDxcc: number | null;
|
||||
};
|
||||
|
||||
export function parseCallsign(callsign: string): CallsignData | null {
|
||||
callsign = callsign.toUpperCase();
|
||||
const match = callsign.match(callsignPattern);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const secondaryPrefix = match[1]?.slice(0, -1) ?? null;
|
||||
const base = match[2];
|
||||
const secondarySuffix = match[3]?.slice(1) ?? null;
|
||||
const baseDxcc = findDxcc(base + '/' + secondarySuffix);
|
||||
const prefixDxcc = secondaryPrefix ? findDxcc(callsign) : null;
|
||||
|
||||
const basePrefix = baseDxcc ? base.slice(0, baseDxcc.prefixLength) : null;
|
||||
const baseSuffix = baseDxcc ? base.slice(baseDxcc.prefixLength) : null;
|
||||
|
||||
const suffixPartOf = prefixDxcc?.withSuffix
|
||||
? 'prefix'
|
||||
: !prefixDxcc && baseDxcc?.withSuffix
|
||||
? 'base'
|
||||
: null;
|
||||
|
||||
return {
|
||||
secondaryPrefix,
|
||||
basePrefix,
|
||||
baseSuffix,
|
||||
base,
|
||||
secondarySuffix,
|
||||
suffixPartOf,
|
||||
baseDxcc: baseDxcc?.entity || null,
|
||||
prefixDxcc: prefixDxcc?.entity || null
|
||||
};
|
||||
}
|
||||
|
||||
export function getSecondarySuffixDescription(callsign: CallsignData): string | null {
|
||||
if (!callsign.secondarySuffix) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (callsign.suffixPartOf === 'base') {
|
||||
return 'Part of prefix';
|
||||
}
|
||||
|
||||
if (callsign.suffixPartOf === 'prefix') {
|
||||
return 'Part of secondary prefix';
|
||||
}
|
||||
|
||||
switch (callsign.secondarySuffix) {
|
||||
case 'P':
|
||||
return 'Portable';
|
||||
case 'M':
|
||||
return 'Mobile';
|
||||
case 'AM':
|
||||
return 'Aeronautical mobile';
|
||||
case 'MM':
|
||||
return 'Maritime mobile';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { dxccTree, findDxcc } from './dxcc-util';
|
||||
import { dxccEntities, dxccTree, findDxcc } from './dxcc-util';
|
||||
|
||||
describe('dxccTree', () => {
|
||||
test('dxccTree is not null', () => {
|
||||
@ -100,3 +100,17 @@ describe('findDxcc', () => {
|
||||
expect(result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dxccEntities', () => {
|
||||
test('dxccEntities is not null', () => {
|
||||
expect(dxccEntities).not.toBe(null);
|
||||
});
|
||||
|
||||
test('499', () => {
|
||||
const entity = dxccEntities.get(499);
|
||||
expect(entity).not.toBe(undefined);
|
||||
expect(entity?.name).toBe('Slovenia');
|
||||
expect(entity?.cont).toBe('EU');
|
||||
expect(entity?.cqz).toBe(15);
|
||||
});
|
||||
});
|
||||
|
@ -1,4 +1,5 @@
|
||||
import dxccTreeFile from '../assets/dxcc-tree.txt?raw';
|
||||
import dxccEntitiesFile from '../assets/dxcc-entities.json';
|
||||
import { TrieNode } from './models/trie';
|
||||
|
||||
export const dxccTree = TrieNode.decodeFromString(dxccTreeFile);
|
||||
@ -38,3 +39,5 @@ export function findDxcc(prefix: string, startingNode: TrieNode = dxccTree): Dxc
|
||||
if (!entity) return null;
|
||||
return { entity, withSuffix: false, prefixLength };
|
||||
}
|
||||
|
||||
export const dxccEntities = new Map([...dxccEntitiesFile].map((e) => [e.entity, e]));
|
||||
|
@ -55,6 +55,10 @@ export class TrieNode {
|
||||
}
|
||||
|
||||
encodeToString(): string {
|
||||
return [...this.getAllNodes()].map((n) => n._encodeToString()).join('\n');
|
||||
}
|
||||
|
||||
_encodeToString(): string {
|
||||
const s = [];
|
||||
if (this.entity) {
|
||||
s.push(`${this.id}=${this.entity}`);
|
||||
|
40
src/lib/string-util.test.ts
Normal file
40
src/lib/string-util.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { capitalize } from './string-util';
|
||||
|
||||
describe('capitalize', () => {
|
||||
test('Single word', () => {
|
||||
expect(capitalize('hello')).toBe('Hello');
|
||||
});
|
||||
|
||||
test('Multiple words', () => {
|
||||
expect(capitalize('hello world')).toBe('Hello World');
|
||||
});
|
||||
|
||||
test('Empty string', () => {
|
||||
expect(capitalize('')).toBe('');
|
||||
});
|
||||
|
||||
test('Single letter', () => {
|
||||
expect(capitalize('a')).toBe('A');
|
||||
});
|
||||
|
||||
test('Repeating letters', () => {
|
||||
expect(capitalize('aaa')).toBe('Aaa');
|
||||
});
|
||||
|
||||
test('Repeating words', () => {
|
||||
expect(capitalize('hello hello')).toBe('Hello Hello');
|
||||
});
|
||||
|
||||
test('Mixed case', () => {
|
||||
expect(capitalize('hELLO')).toBe('Hello');
|
||||
});
|
||||
|
||||
test('Mixed case words', () => {
|
||||
expect(capitalize('hELLO wORLD')).toBe('Hello World');
|
||||
});
|
||||
|
||||
test('Repeating substrings', () => {
|
||||
expect(capitalize('hihihi hi')).toBe('Hihihi Hi');
|
||||
});
|
||||
});
|
@ -1,12 +1,6 @@
|
||||
export const capitalize = (s: string) => {
|
||||
s = s.toLowerCase();
|
||||
const re = /\b\w+/g;
|
||||
while (true) {
|
||||
const result = re.exec(s);
|
||||
if (!result) break;
|
||||
s = s.replace(result[0], result[0][0].toUpperCase() + result[0].slice(1));
|
||||
}
|
||||
return s;
|
||||
return s.replace(/\b\w+/g, (word) => word[0].toUpperCase() + word.slice(1));
|
||||
};
|
||||
|
||||
export const rangeToString = (start: string, end: string): string => {
|
||||
|
@ -2,4 +2,39 @@
|
||||
import '../app.css';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Callsign Tester</title>
|
||||
<meta name="description" content="Callsign tester for ham radio operators" />
|
||||
<meta name="keywords" content="callsign, ham radio, dxcc" />
|
||||
<meta name="author" content="Jakob Kordež S52KJ" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex min-h-screen flex-col bg-[#333] text-[#eee]">
|
||||
<div class="c flex-1">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<footer class="bg-[#444] py-3">
|
||||
<div class="c flex justify-between gap-4">
|
||||
<div>
|
||||
By <a href="https://jkob.cc">S52KJ</a>
|
||||
</div>
|
||||
<div>
|
||||
Data from <a href="https://clublog.org">ClubLog</a>
|
||||
</div>
|
||||
<div>
|
||||
Source code on <a href="https://github.com/jakobkordez/call-tester">GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.c {
|
||||
@apply mx-auto w-full max-w-3xl px-6;
|
||||
}
|
||||
|
||||
footer a {
|
||||
@apply text-[#bbe];
|
||||
}
|
||||
</style>
|
||||
|
@ -1,30 +1,79 @@
|
||||
<script lang="ts">
|
||||
import { callsignPattern } from '$lib/callsign';
|
||||
import { dxccTree, findDxcc } from '$lib/dxcc-util';
|
||||
import { getSecondarySuffixDescription, parseCallsign } from '$lib/callsign';
|
||||
import { dxccEntities } from '$lib/dxcc-util';
|
||||
import CallsignInput from '../components/callsign-input.svelte';
|
||||
|
||||
const baseClass = 'text-red-400';
|
||||
const prefixClass = 'text-blue-400';
|
||||
const suffixClass = 'text-green-400';
|
||||
|
||||
let callsign = '9a/s52kj/p';
|
||||
|
||||
function generateStyledText(text: string): string {
|
||||
const match = text.match(callsignPattern);
|
||||
if (!match) return text;
|
||||
$: callsignData = parseCallsign(callsign);
|
||||
|
||||
const [, prefix, base, suffix] = match;
|
||||
const baseDxcc = findDxcc(base);
|
||||
const prefixDxcc = prefix ? findDxcc(text) : null;
|
||||
$: suffixPartOf = [null, 'base', 'prefix'].indexOf(callsignData?.suffixPartOf ?? null);
|
||||
|
||||
function styleText(): string {
|
||||
if (!callsignData) return callsign;
|
||||
|
||||
const { base, basePrefix, baseSuffix, secondaryPrefix, secondarySuffix } = callsignData;
|
||||
|
||||
// TODO Check if base and prefix same dxcc
|
||||
const suffClass = [suffixClass, baseClass, prefixClass][suffixPartOf];
|
||||
return [
|
||||
prefixDxcc ? `<span class="prefix">${prefix}</span>` : prefix,
|
||||
`<span class="base">${base}</span>`,
|
||||
`<span class="dxcc">${base}</span>`,
|
||||
suffix ? `<span class="suffix">${suffix}</span>` : null
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('');
|
||||
secondaryPrefix ? `<span class="${prefixClass}">${secondaryPrefix}/</span>` : '',
|
||||
basePrefix ? `<span class="${baseClass}">${basePrefix}</span>${baseSuffix}` : base,
|
||||
secondarySuffix ? `<span class="${suffClass}">/${secondarySuffix}</span>` : ''
|
||||
].join('');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mx-auto py-10 max-w-3xl flex flex-col gap-6 px-6">
|
||||
<h1 class="text-3xl font-medium text-center">Callsign Tester</h1>
|
||||
<div class="flex flex-col gap-6 py-10">
|
||||
<h1 class="text-center text-3xl font-medium">Callsign Tester</h1>
|
||||
|
||||
<CallsignInput bind:inputText={callsign} {generateStyledText} />
|
||||
<div>
|
||||
<div class="text-center">Enter a callsign</div>
|
||||
<CallsignInput bind:inputText={callsign} generateStyledText={styleText} />
|
||||
</div>
|
||||
|
||||
{#if callsignData}
|
||||
<div class="flex flex-col gap-4 md:flex-row">
|
||||
{#if callsignData.prefixDxcc}
|
||||
<div class="data-box prefix">
|
||||
<h2 class="text-xl">Secondary prefix</h2>
|
||||
<div class="font-mono text-2xl font-medium">{callsignData.secondaryPrefix}</div>
|
||||
<div>{dxccEntities.get(callsignData.prefixDxcc)?.name}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if callsignData.baseDxcc}
|
||||
<div class="data-box base">
|
||||
<h2 class="text-xl">Prefix</h2>
|
||||
<div class="font-mono text-2xl font-medium">{callsignData.basePrefix}</div>
|
||||
<div>{dxccEntities.get(callsignData.baseDxcc)?.name}</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if callsignData.secondarySuffix}
|
||||
<div class={`data-box ${['suffix', 'base', 'prefix'][suffixPartOf]}`}>
|
||||
<h2 class="text-xl">Secondary suffix</h2>
|
||||
<div class="font-mono text-2xl font-medium">{callsignData.secondarySuffix}</div>
|
||||
<div>{getSecondarySuffixDescription(callsignData) ?? ''}</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="postcss">
|
||||
.data-box {
|
||||
@apply flex-1 rounded-xl p-4;
|
||||
}
|
||||
.data-box.prefix {
|
||||
@apply bg-blue-700/40;
|
||||
}
|
||||
.data-box.base {
|
||||
@apply bg-red-700/40;
|
||||
}
|
||||
.data-box.suffix {
|
||||
@apply bg-green-700/40;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import adapter from '@sveltejs/adapter-auto';
|
||||
import adapter from '@sveltejs/adapter-static';
|
||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
@ -11,7 +11,9 @@ const config = {
|
||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||
adapter: adapter()
|
||||
adapter: adapter({
|
||||
fallback: 'index.html'
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -2,8 +2,7 @@
|
||||
export default {
|
||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {}
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
plugins: []
|
||||
};
|
||||
|
51
yarn.lock
51
yarn.lock
@ -360,6 +360,11 @@
|
||||
dependencies:
|
||||
import-meta-resolve "^4.1.0"
|
||||
|
||||
"@sveltejs/adapter-static@^3.0.2":
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@sveltejs/adapter-static/-/adapter-static-3.0.2.tgz#b505c429616c3319d40d293b741f6915da143f49"
|
||||
integrity sha512-/EBFydZDwfwFfFEuF1vzUseBoRziwKP7AoHAwv+Ot3M084sE/HTVBHf9mCmXfdM9ijprY5YEugZjleflncX5fQ==
|
||||
|
||||
"@sveltejs/kit@^2.0.0":
|
||||
version "2.5.17"
|
||||
resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-2.5.17.tgz#e59e00be7a86021c897ce65a540740473535898e"
|
||||
@ -422,9 +427,9 @@
|
||||
integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
|
||||
|
||||
"@types/node@*", "@types/node@^20.14.6":
|
||||
version "20.14.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.6.tgz#f3c19ffc98c2220e18de259bb172dd4d892a6075"
|
||||
integrity sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==
|
||||
version "20.14.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.14.8.tgz#45c26a2a5de26c3534a9504530ddb3b27ce031ac"
|
||||
integrity sha512-DO+2/jZinXfROG7j7WKFn/3C6nFwxy2lLpgLjEXJz+0XKphZlTLJ14mo8Vfg8X5BWN6XjyESXq+LcYdT7tR3bA==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
@ -920,9 +925,9 @@ eastasianwidth@^0.2.0:
|
||||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
||||
|
||||
electron-to-chromium@^1.4.796:
|
||||
version "1.4.807"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.807.tgz#4d6c5ea1516f0164ac5bfd487ccd4ee9507c8f01"
|
||||
integrity sha512-kSmJl2ZwhNf/bcIuCH/imtNOKlpkLDn2jqT5FJ+/0CXjhnFaOa9cOe9gHKKy71eM49izwuQjZhKk+lWQ1JxB7A==
|
||||
version "1.4.810"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.810.tgz#7dee01b090b9e048e6db752f7b30921790230654"
|
||||
integrity sha512-Kaxhu4T7SJGpRQx99tq216gCq2nMxJo+uuT6uzz9l8TVN2stL7M06MIIXAtr9jsrLs2Glflgf2vMQRepxawOdQ==
|
||||
|
||||
emoji-regex@^8.0.0:
|
||||
version "8.0.0"
|
||||
@ -1930,7 +1935,12 @@ prettier-plugin-svelte@^3.1.2:
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.5.tgz#ec004ed6626f59655cfd3fe88154c7cf41c90a2e"
|
||||
integrity sha512-vP/M/Goc8z4iVIvrwXwbrYVjJgA0Hf8PO1G4LBh/ocSt6vUP6sLvyu9F3ABEGr+dbKyxZjEKLkeFsWy/yYl0HQ==
|
||||
|
||||
prettier@^3.1.1:
|
||||
prettier-plugin-tailwindcss@^0.6.5:
|
||||
version "0.6.5"
|
||||
resolved "https://registry.yarnpkg.com/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.5.tgz#e05202784a3f41889711ae38c75c5b8cad72f368"
|
||||
integrity sha512-axfeOArc/RiGHjOIy9HytehlC0ZLeMaqY09mm8YCkMzznKiDkwFzOpBvtuhuv3xG5qB73+Mj7OCe2j/L1ryfuQ==
|
||||
|
||||
prettier@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.2.tgz#03ff86dc7c835f2d2559ee76876a3914cec4a90a"
|
||||
integrity sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==
|
||||
@ -2124,16 +2134,8 @@ std-env@^3.5.0:
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.7.0.tgz#c9f7386ced6ecf13360b6c6c55b8aaa4ef7481d2"
|
||||
integrity sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==
|
||||
|
||||
"string-width-cjs@npm:string-width@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
dependencies:
|
||||
emoji-regex "^8.0.0"
|
||||
is-fullwidth-code-point "^3.0.0"
|
||||
strip-ansi "^6.0.1"
|
||||
|
||||
string-width@^4.1.0:
|
||||
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0:
|
||||
name string-width-cjs
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
|
||||
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
|
||||
@ -2151,14 +2153,7 @@ string-width@^5.0.1, string-width@^5.1.2:
|
||||
emoji-regex "^9.2.2"
|
||||
strip-ansi "^7.0.1"
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
|
||||
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
|
||||
version "6.0.1"
|
||||
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
|
||||
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
|
||||
@ -2401,9 +2396,9 @@ typescript-eslint@^8.0.0-alpha.20:
|
||||
"@typescript-eslint/utils" "8.0.0-alpha.30"
|
||||
|
||||
typescript@^5.0.0, typescript@^5.0.3:
|
||||
version "5.4.5"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"
|
||||
integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.2.tgz#c26f023cb0054e657ce04f72583ea2d85f8d0507"
|
||||
integrity sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==
|
||||
|
||||
ufo@^1.5.3:
|
||||
version "1.5.3"
|
||||
|
Loading…
x
Reference in New Issue
Block a user