This commit is contained in:
Jakob Kordež 2023-07-14 23:37:59 +02:00
parent d668df566c
commit c887c724af
15 changed files with 398 additions and 149 deletions

View File

@ -1,3 +1,6 @@
{
"extends": ["next/core-web-vitals", "prettier"]
"extends": ["next/core-web-vitals", "prettier"],
"rules": {
"no-unused-vars": "warn"
}
}

View File

@ -84,7 +84,7 @@ export default function IzpitQuiz() {
function inProgress() {
return (
<div className="mx-auto my-10 flex max-w-xl flex-col gap-12">
<div className="container my-10 flex max-w-xl flex-col gap-12">
{questions?.map((question, qi) => (
<QuestionCard
key={qi}
@ -133,7 +133,7 @@ export default function IzpitQuiz() {
<div className="my-10">
<h1 className="my-10 text-center text-2xl">Napačni odgovori</h1>
<div className="mx-auto flex max-w-xl flex-col gap-12">
<div className="container flex max-w-xl flex-col gap-12">
{questions?.map(
(question, qi) =>
question.correct !== answers![qi][0] && (

View File

@ -0,0 +1,114 @@
'use client';
import { robotoMono } from '@/fonts/fonts';
import {
faCheckCircle,
faXmarkCircle,
} from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useState } from 'react';
export default function CallsignTool() {
const [clas, setClas] = useState(1);
const [callsign, setCallsign] = useState('S50HQ');
return (
<div className="container my-10 flex flex-col gap-6">
<div className="flex flex-col gap-1">
<label className="text-sm font-semibold">Izberi razred</label>
<div className="flex flex-row gap-4">
<button
onClick={() => setClas(0)}
className={`flex-1 rounded-lg border-4 bg-light py-2 font-semibold text-dark shadow ${
!clas ? 'border-dark' : 'border-transparent'
}`}
>
N razred
</button>
<button
onClick={() => setClas(1)}
className={`flex-1 rounded-lg border-4 bg-light py-2 font-semibold text-dark shadow ${
clas ? 'border-dark' : 'border-transparent'
}`}
>
A razred
</button>
</div>
</div>
<div className="flex flex-col gap-1">
<label className="text-sm font-semibold">Vnesi klicni znak</label>
<input
type="text"
className={`rounded-lg border border-light py-3 text-center text-3xl uppercase shadow-lg ${robotoMono.className}`}
value={callsign}
onChange={(e) => setCallsign(e.target.value)}
/>
</div>
<div className="flex flex-col overflow-clip rounded-lg">
{tests
.filter((test) =>
test.preTest ? test.preTest(clas, callsign) : true
)
.map((test, i) => {
const result = test.test(callsign);
return (
<div
key={i}
className={`flex flex-row items-center gap-4 px-5 py-3 text-lg ${
result ? 'bg-green-100' : 'bg-red-100'
}`}
>
<FontAwesomeIcon
icon={result ? faCheckCircle : faXmarkCircle}
className={`w-5 ${
result ? 'text-green-600' : 'text-red-600'
}`}
/>
<span>{test.name}</span>
</div>
);
})}
</div>
</div>
);
}
const tests = [
{
name: 'Se začne s S5',
test: (callsign: string) => /^S5/i.test(callsign),
},
{
name: 'Vsebuje eno številko',
preTest: (clas: number, callsign: string) => /^S5/i.test(callsign),
test: (callsign: string) => /^S5\d/i.test(callsign),
},
{
name: 'Vsebuje eno do tri črke',
preTest: (clas: number, callsign: string) =>
clas === 1 && /^S5\d/i.test(callsign),
test: (callsign: string) => /^S5\d[A-Z]{1,3}$/i.test(callsign),
},
{
name: 'Vsebuje tri črke',
preTest: (clas: number, callsign: string) =>
clas === 0 && /^S5\d/i.test(callsign),
test: (callsign: string) => /^S5\d[A-Z]{3}$/i.test(callsign),
},
{
name: 'Ustreza razredu A',
preTest: (clas: number, callsign: string) =>
clas === 1 && /^S5\d[A-Z]{1,3}$/i.test(callsign),
test: (callsign: string) =>
/^S5(\d[A-Z]{1,2}|([0457][A-X]|[4678]Z)[A-Z]{2})$/i.test(callsign),
},
{
name: 'Ustreza razredu N',
preTest: (clas: number, callsign: string) =>
clas === 0 && /^S5\d[A-Z]{3}$/i.test(callsign),
test: (callsign: string) => /^S5(2[A-XZ]|8[A-X])[A-Z]{2}$/i.test(callsign),
},
];

View File

@ -0,0 +1,27 @@
import { SubHeader } from '@/components/sub_header';
import { Metadata } from 'next';
import CallsignTool from './callsign-tool';
export const metadata: Metadata = {
title: 'Izbira klicnega znaka',
description: 'Pomoč pri izbiri klicnega znaka',
};
export default function Callsign() {
return (
<>
<SubHeader title="Pomoč pri izbiri klicnega znaka">
<p className="text-lg">
Spodaj vpiši želen klicni znak in preveri, če je ta že zaseden in če
je za pravi razred.
</p>
<p className="text-lg">
Če v znaku uporabiš * (zvezdico), ti bodo prikazani vsi klicni znaki,
ki ustrezajo vnesenemu vzorcu.
</p>
</SubHeader>
<CallsignTool />
</>
);
}

View File

@ -2,6 +2,7 @@ import { AnalyticsWrapper } from '@/components/analytics';
import Header from '@/components/header';
import { Metadata } from 'next';
import '@/styles/globals.scss';
import { morse } from '@/fonts/fonts';
export const metadata: Metadata = {
title: {
@ -42,7 +43,7 @@ export default function RootLayout({
return (
<html lang="sl">
<head />
<body>
<body className={morse.variable}>
<Header />
<main>{children}</main>

View File

@ -73,7 +73,7 @@ export default function License() {
<p>
Klicni znak si lahko izbereš glede na razred izpita, ki si ga opravil.
</p>
<div className="flex flex-col gap-6 md:flex-row">
<div className="my-6 flex flex-col gap-6 md:flex-row">
<div className="flex flex-1 flex-col gap-4 rounded-lg bg-light p-6 shadow-md">
<h4 className="text-center">N razred</h4>
@ -96,6 +96,10 @@ export default function License() {
</ul>
</div>
</div>
<LinkButton href="/klicni-znak" className="w-full">
Pomagaj izbrati klicni znak
</LinkButton>
</div>
<div className="flex bg-light">
@ -106,7 +110,23 @@ export default function License() {
digitalnim potrdilom), ali pa z izpolnjenim obrazcem poslanega po
pošti.
</p>
<div className="flex flex-col gap-4 text-center md:flex-row">
<p>
Priporočljivo je, da na vlogi izpolniš vse tri klicne znake, ki jih
želiš imeti v primeru, da je prvi klicni znak že zaseden ali pa ga
zavrnejo.
</p>
<p>
Od začetka leta 2023 se za izdajo in podaljšanje radioamaterskega
dovoljenja <strong>plača uporabo dela radijskega spektra</strong>{' '}
(frekvenčnino) v višini 60 točk. Točka je vredna 0,50 .
</p>
<p>
Radioamatersko dovoljenje velja 15 let, po tem času pa ga je
potrebno podaljšati. Po poteku veljavnosti velja še 10 letni
moratorij za klicni znak.
</p>
<div className="mt-6 flex flex-col gap-4 text-center md:flex-row">
<LinkButton className="flex-1" href="https://evloge.akos-rs.si/">
Elektronska vloga
</LinkButton>

View File

@ -1,13 +1,11 @@
'use client';
import { morse } from '@/fonts/fonts';
import { useEffect, useState } from 'react';
import { Roboto_Mono } from 'next/font/google';
const robotoMono = Roboto_Mono({ subsets: ['latin'] });
import { generateRandomCallsign } from '@/util/callsign-util';
import { robotoMono } from '@/fonts/fonts';
export default function RandomCallsign() {
const [callsign, setCallsign] = useState<string[]>('S50ZRS'.split(''));
const [callsign, setCallsign] = useState<string>('S50ZRS');
useEffect(() => {
const interval = setInterval(() => {
@ -20,26 +18,9 @@ export default function RandomCallsign() {
return (
<div className="my-6 text-center">
<div className={`text-4xl font-bold text-darker ${robotoMono.className}`}>
{callsign.join('')}
</div>
<div className={`font-bold text-gray-500 ${morse.className}`}>
{callsign.join(' ')}
{callsign}
</div>
<div className={`morse font-bold text-gray-500`}>{callsign}</div>
</div>
);
}
function generateRandomCallsign(): string[] {
const length = Math.ceil(Math.random() * 3);
const callsign = [
'S',
'5',
String.fromCharCode(Math.floor(Math.random() * 10) + 48),
];
for (let i = 0; i < length; i++) {
callsign.push(String.fromCharCode(Math.floor(Math.random() * 26) + 65));
}
return callsign;
}

View File

@ -1,4 +1,4 @@
import { faCheckCircle, faCircle } from '@fortawesome/free-regular-svg-icons';
import { faCheckCircle } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Link from 'next/link';
@ -24,117 +24,122 @@ const povezave = [
export default function Home() {
return (
<>
<div className="content container my-10">
<div className="content container my-12">
<h3>Kaj je radioamaterstvo?</h3>
<p>.... --- .-- / ... .... --- ..- .-.. -.. / .. / -.- -. --- .--</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
</p>
</div>
<h3>Primerjava kategorij</h3>
<div className="flex flex-col gap-6 md:flex-row">
<div className="flex flex-1 flex-col gap-4 rounded-lg bg-light p-6 shadow-md">
<h4 className="text-center">N razred</h4>
<p>
N razred je namenjen začetnikom, ki se šele spoznavajo z
radioamaterstvom in niso še tako vešči v elektroniki.
</p>
<div className="bg-light">
<div className="content container py-12">
<h3 className="text-center">Primerjava kategorij</h3>
<div className="flex flex-col items-center justify-center gap-6 md:flex-row md:items-stretch">
<div className="flex max-w-sm flex-1 flex-col gap-4 rounded-2xl bg-white p-6">
<h4 className="text-center">N razred</h4>
<p>
N razred je namenjen začetnikom, ki se šele spoznavajo z
radioamaterstvom in niso še tako vešči v elektroniki.
</p>
<div className="border-t border-gray-400" />
<div className="border-t border-gray-400" />
<ul className="flex flex-col gap-2">
<li className="flex flex-row items-center gap-2">
<FontAwesomeIcon
icon={faCircle}
className="h-4 text-gray-500"
/>
<span>Uporaba le glavnih radioamaterskih frekvenc</span>
</li>
<li className="flex flex-row items-center gap-2">
<FontAwesomeIcon
icon={faCircle}
className="h-4 text-gray-500"
/>
<span>Manjša moč</span>
</li>
<li className="flex flex-row items-center gap-2">
<FontAwesomeIcon
icon={faCircle}
className="h-4 text-gray-500"
/>
<span>Ozek izbor klicnih znakov</span>
</li>
</ul>
<ul className="flex flex-col gap-2">
<li className="flex flex-row items-baseline gap-2">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-3 text-primary"
/>
<span>Uporaba le glavnih radioamaterskih frekvenc</span>
</li>
<li className="flex flex-row items-baseline gap-2">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-3 text-primary"
/>
<span>Manjša moč</span>
</li>
<li className="flex flex-row items-baseline gap-2">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-3 text-primary"
/>
<span>Ozek izbor klicnih znakov</span>
</li>
</ul>
<div className="border-t border-gray-400" />
<div className="border-t border-gray-400" />
<h5 className="text-center">Klicni znaki</h5>
<ul>
<li>S52AAA - S52XZZ in S52ZAA - S52ZZZ</li>
<li>S58AAA - S58XZZ</li>
</ul>
</div>
<div className="flex flex-1 flex-col gap-4 rounded-lg bg-light p-6 shadow-md">
<h4 className="text-center">A razred</h4>
<p>
A razred je namenjen tistim, ki želijo delovati na vseh amaterskih
frekvencah in z večjo močjo.
</p>
<h5 className="text-center">Klicni znaki</h5>
<ul>
<li>S52AAA - S52XZZ in S52ZAA - S52ZZZ</li>
<li>S58AAA - S58XZZ</li>
</ul>
</div>
<div className="flex max-w-sm flex-1 flex-col gap-4 rounded-2xl bg-dark p-6 text-white">
<h4 className="text-center">A razred</h4>
<p>
A razred je namenjen tistim, ki želijo delovati na vseh
amaterskih frekvencah in z večjo močjo.
</p>
<div className="border-t border-gray-400" />
<div className="border-t border-gray-400" />
<ul className="flex flex-col gap-2">
<li className="flex flex-row items-center gap-2">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-4 text-green-500"
/>
<span>Uporaba vseh radioamaterskih frekvenc</span>
</li>
<li className="flex flex-row items-center gap-2">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-4 text-green-500"
/>
<span>Večja moč</span>
</li>
<li className="flex flex-row items-center gap-2">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-4 text-green-500"
/>
<span>Širši izbor klicnih znakov</span>
</li>
<li className="flex flex-row items-center gap-2">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-4 text-green-500"
/>
<span>Uporaba radioamaterskih satelitskih storitev</span>
</li>
</ul>
<ul className="flex flex-col gap-2">
<li className="flex flex-row items-baseline gap-2">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-3 text-primary"
/>
<span>Uporaba vseh radioamaterskih frekvenc</span>
</li>
<li className="flex flex-row items-baseline gap-2">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-3 text-primary"
/>
<span>Večja moč</span>
</li>
<li className="flex flex-row items-baseline gap-2">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-3 text-primary"
/>
<span>Širši izbor klicnih znakov</span>
</li>
<li className="flex flex-row items-baseline gap-2">
<FontAwesomeIcon
icon={faCheckCircle}
className="h-3 text-primary"
/>
<span>Uporaba radioamaterskih satelitskih storitev</span>
</li>
</ul>
<div className="border-t border-gray-400" />
<div className="border-t border-gray-400" />
<h5 className="text-center">Klicni znaki</h5>
<ul>
<li>S50A - S59Z</li>
<li>S50AA - S59ZZ</li>
<li>S50AAA - S50XZZ</li>
<li>S54AAA - S54XZZ in S54ZAA - S54ZZZ</li>
<li>S56AAA - S56XZZ in S56ZAA - S56ZZZ</li>
<li>S57AAA - S57XZZ in S57ZAA - S57ZZZ</li>
<li>S58ZAA - S58ZZZ</li>
</ul>
<h5 className="text-center">Klicni znaki</h5>
<ul>
<li>S50A - S59Z</li>
<li>S50AA - S59ZZ</li>
<li>S50AAA - S50XZZ</li>
<li>S54AAA - S54XZZ in S54ZAA - S54ZZZ</li>
<li>S56AAA - S56XZZ in S56ZAA - S56ZZZ</li>
<li>S57AAA - S57XZZ in S57ZAA - S57ZZZ</li>
<li>S58ZAA - S58ZZZ</li>
</ul>
</div>
</div>
</div>
</div>
<h5>Preizkus sprejema in oddaje Morzejevih znakov</h5>
<p>
Kandidat, ki na lastno željo opravlja izpit iz predmeta Sprejem in
oddaja Morzejevih znakov, mora dokazati, da je sposoben v Morzejevih
znakih (mednarodni Morse-kod) sprejemati na sluh in s tipkalom
oddajati odprti tekst, skupine številk, ločila in druge znake pri
hitrosti 25 znakov na minuto.
</p>
<div className="content container my-12">
<h3>O izpitu</h3>
<p>Na izpitu se preverja znanje iz naslednjih področij:</p>
<ul>
@ -157,6 +162,15 @@ export default function Home() {
, ki se lahko pojavijo na izpitu.
</p>
<h5>Preizkus sprejema in oddaje Morzejevih znakov</h5>
<p>
Kandidat, ki na lastno željo opravlja izpit iz predmeta Sprejem in
oddaja Morzejevih znakov, mora dokazati, da je sposoben v Morzejevih
znakih (mednarodni Morse-kod) sprejemati na sluh in s tipkalom
oddajati odprti tekst, skupine številk, ločila in druge znake pri
hitrosti 25 znakov na minuto.
</p>
<h3>Vsebine za pripravo na izpit</h3>
<p>
Vsa snov, ki se lahko pojavi v izpitnih vprašanjih je vsebovana v{' '}
@ -217,8 +231,8 @@ export default function Home() {
Po opravljenem izpitu lahko zaprosiš za klicni znak (CEPT licenco) na
agenciji za komunikacijska omrežja in storitve Republike Slovenije
(AKOS). Več o tem si lahko prebereš na podstrani{' '}
<Link className="link" href="/znaki">
Klicni znaki
<Link className="link" href="/licenca">
Licenca
</Link>
.
</p>

View File

@ -121,7 +121,7 @@ export default function VajaQuiz() {
</div>
</SubHeader>
<div className="mx-auto flex max-w-xl flex-col gap-12">
<div className="container flex max-w-xl flex-col gap-12">
{questions.slice(0, displayed).map((question, qi) => (
<QuestionCard
key={qi}
@ -141,7 +141,7 @@ export default function VajaQuiz() {
))}
</div>
<div className="mx-auto w-full max-w-xl">
<div className="container w-full max-w-xl">
<div className="flex flex-row justify-end gap-3">
{questions.length > displayed && (
<Button onClick={loadMore}>Naloži več</Button>

View File

@ -1,8 +1,10 @@
export default function sitemap() {
const pages = ['', '/priprave', '/izpit-sim'].map((page) => ({
url: `https://izpit.jkob.cc${page}`,
lastModified: new Date().toISOString(),
}));
const pages = ['', '/licenca', '/zbirka', '/priprave', '/izpit-sim'].map(
(page) => ({
url: `https://izpit.jkob.cc${page}`,
lastModified: new Date().toISOString(),
})
);
return pages;
}

View File

@ -27,7 +27,7 @@ export function QuestionList() {
}, [questions.length]);
return (
<div className="mx-auto my-10 flex max-w-xl flex-col gap-12">
<div className="container my-10 flex max-w-xl flex-col gap-12">
{questions.map((question) => (
<QuestionCard
key={question.id}

View File

@ -1,10 +1,8 @@
'use client';
import { morse } from '@/fonts/fonts';
import Image from 'next/image';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useState } from 'react';
const nav = [
{ href: '/', label: 'Domov' },
@ -16,8 +14,6 @@ const nav = [
];
export default function Header() {
const [navbar, setNavbar] = useState<boolean>(false);
const pathname = usePathname();
return (
@ -30,11 +26,8 @@ export default function Header() {
<Link href="/">
<h1 className="text-4xl">Radioamaterski izpit</h1>
</Link>
<div
data-nosnippet
className={`mt-1 text-sm text-gray-400 ${morse.className}`}
>
C Q | D E | Z R S
<div data-nosnippet className="morse mt-1 text-sm text-gray-400">
CQ|DE|ZRS
</div>
</div>
</div>
@ -42,7 +35,7 @@ export default function Header() {
</div>
<div className="bg-dark">
<nav className="container flex flex-row justify-start">
<nav className="container flex flex-row flex-wrap justify-start">
{nav.map(({ href, label }) => (
<Link
key={href}

View File

@ -1,3 +1,9 @@
import { Roboto_Mono } from 'next/font/google';
import localFont from 'next/font/local';
export const morse = localFont({ src: './morse.ttf' });
export const morse = localFont({
src: './morse.ttf',
variable: '--font-morse',
});
export const robotoMono = Roboto_Mono({ subsets: ['latin'] });

View File

@ -57,3 +57,13 @@
margin-left: 0.5rem;
}
}
.morse {
font-family: var(--font-morse);
letter-spacing: 0.6em;
}
.text-center .morse,
.text-center.morse {
text-indent: 0.6em;
}

78
src/util/callsign-util.ts Normal file
View File

@ -0,0 +1,78 @@
export function generateRandomCallsign(): string {
const length = Math.ceil(Math.random() * 3);
let callsign = `S5${String.fromCharCode(
Math.floor(Math.random() * 10) + 48
)}`;
for (let i = 0; i < length; i++) {
callsign += String.fromCharCode(Math.floor(Math.random() * 26) + 65);
}
return callsign;
}
export function cwWeight(text: string): number {
text = text.toUpperCase();
let ret = (text.length - 1) * 3;
for (let i = 0; i < text.length; i++) {
if (text[i] === ' ') {
ret += 1;
continue;
}
const cw = cwMap.get(text[i]);
if (!cw) throw new Error(`No cw for ${text[i]}`);
ret += cw.length - 1;
for (const c in cw) {
if (cw[c] == '.') ret += 1;
else ret += 3;
}
}
return ret;
}
const cwMap = new Map<String, String>([
['A', '.-'],
['B', '-...'],
['C', '-.-.'],
['D', '-..'],
['E', '.'],
['F', '..-.'],
['G', '--.'],
['H', '....'],
['I', '..'],
['J', '.---'],
['K', '-.-'],
['L', '.-..'],
['M', '--'],
['N', '-.'],
['O', '---'],
['P', '.--.'],
['Q', '--.-'],
['R', '.-.'],
['S', '...'],
['T', '-'],
['U', '..-'],
['V', '...-'],
['W', '.--'],
['X', '-..-'],
['Y', '-.--'],
['Z', '--..'],
['0', '-----'],
['1', '.----'],
['2', '..---'],
['3', '...--'],
['4', '....-'],
['5', '.....'],
['6', '-....'],
['7', '--...'],
['8', '---..'],
['9', '----.'],
['.', '.-.-.-'],
[',', '--..--'],
['?', '..--..'],
['/', '-..-.'],
['=', '-...-'],
]);