mirror of
https://github.com/jakobkordez/s5_practice.git
synced 2025-05-15 16:20:31 +00:00
Minor redesign and other changes
This commit is contained in:
parent
5c97b5d402
commit
79060159e0
@ -16,20 +16,20 @@
|
|||||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||||
"@serwist/next": "^9.0.3",
|
"@serwist/next": "^9.0.3",
|
||||||
"@vercel/analytics": "^1.0.2",
|
"@vercel/analytics": "^1.0.2",
|
||||||
|
"katex": "^0.16.11",
|
||||||
"next": "^14.1.4",
|
"next": "^14.1.4",
|
||||||
"postcss": "^8.4.30",
|
"postcss": "^8.4.30",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-katex": "^3.0.1",
|
|
||||||
"sharp": "^0.32.6",
|
"sharp": "^0.32.6",
|
||||||
"zustand": "^4.4.1"
|
"zustand": "^4.4.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.10",
|
"@tailwindcss/typography": "^0.5.10",
|
||||||
|
"@types/katex": "^0.16.7",
|
||||||
"@types/node": "^20.6.3",
|
"@types/node": "^20.6.3",
|
||||||
"@types/react": "^18.2.22",
|
"@types/react": "^18.2.22",
|
||||||
"@types/react-dom": "^18.2.6",
|
"@types/react-dom": "^18.2.6",
|
||||||
"@types/react-katex": "^3.0.0",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
"@typescript-eslint/eslint-plugin": "^7.4.0",
|
||||||
"@typescript-eslint/parser": "^7.4.0",
|
"@typescript-eslint/parser": "^7.4.0",
|
||||||
"autoprefixer": "^10.4.15",
|
"autoprefixer": "^10.4.15",
|
||||||
|
@ -4,11 +4,6 @@ import Generator from './generator';
|
|||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Generator izpitnih pol',
|
title: 'Generator izpitnih pol',
|
||||||
description: 'Pripomoček za generiranje izpitnih pol za radioamaterski izpit',
|
description: 'Pripomoček za generiranje izpitnih pol za radioamaterski izpit',
|
||||||
openGraph: {
|
|
||||||
title: 'Generator izpitnih pol',
|
|
||||||
description:
|
|
||||||
'Pripomoček za generiranje izpitnih pol za radioamaterski izpit',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Izpit() {
|
export default function Izpit() {
|
||||||
|
@ -5,6 +5,7 @@ import { getExamQuestions } from '@/util/question-util';
|
|||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
import QuestionCard from '@/components/question_card';
|
import QuestionCard from '@/components/question_card';
|
||||||
import { scrollToTop } from '@/components/scroll-to-top-button';
|
import { scrollToTop } from '@/components/scroll-to-top-button';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
enum QuizState {
|
enum QuizState {
|
||||||
Loading,
|
Loading,
|
||||||
@ -19,18 +20,20 @@ interface IzpitQuizStore {
|
|||||||
questions?: Question[];
|
questions?: Question[];
|
||||||
answers?: number[][];
|
answers?: number[][];
|
||||||
correctCount?: number;
|
correctCount?: number;
|
||||||
|
endTime?: Date;
|
||||||
|
|
||||||
load: () => Promise<void>;
|
load: () => Promise<void>;
|
||||||
finish: (correctCount: number) => Promise<void>;
|
finish: () => Promise<void>;
|
||||||
reset: () => Promise<void>;
|
reset: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStore = create<IzpitQuizStore>((set) => ({
|
const useStore = create<IzpitQuizStore>((set, get) => ({
|
||||||
state: QuizState.Ready,
|
state: QuizState.Ready,
|
||||||
|
|
||||||
questions: undefined,
|
questions: undefined,
|
||||||
answers: undefined,
|
answers: undefined,
|
||||||
correctCount: undefined,
|
correctCount: undefined,
|
||||||
|
endTime: undefined,
|
||||||
|
|
||||||
load: async () => {
|
load: async () => {
|
||||||
set({ state: QuizState.Loading });
|
set({ state: QuizState.Loading });
|
||||||
@ -41,10 +44,17 @@ const useStore = create<IzpitQuizStore>((set) => ({
|
|||||||
state: QuizState.InProgress,
|
state: QuizState.InProgress,
|
||||||
questions,
|
questions,
|
||||||
answers: Array(questions.length).fill([-1]),
|
answers: Array(questions.length).fill([-1]),
|
||||||
|
endTime: new Date(Date.now() + 1000 * 60 * 90),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
finish: async (correctCount: number) => {
|
finish: async () => {
|
||||||
|
const { questions, answers } = get();
|
||||||
|
|
||||||
|
const correctCount = questions!
|
||||||
|
.map((q, qi) => q.correct === answers![qi][0])
|
||||||
|
.reduce((acc, cur) => acc + (cur ? 1 : 0), 0);
|
||||||
|
|
||||||
set({ state: QuizState.Finished, correctCount });
|
set({ state: QuizState.Finished, correctCount });
|
||||||
scrollToTop();
|
scrollToTop();
|
||||||
},
|
},
|
||||||
@ -55,16 +65,25 @@ const useStore = create<IzpitQuizStore>((set) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
export default function IzpitQuiz() {
|
export default function IzpitQuiz() {
|
||||||
const [state, questions, answers, correctCount, load, finish, reset] =
|
const [
|
||||||
useStore((state) => [
|
state,
|
||||||
state.state,
|
questions,
|
||||||
state.questions,
|
answers,
|
||||||
state.answers,
|
correctCount,
|
||||||
state.correctCount,
|
endTime,
|
||||||
state.load,
|
load,
|
||||||
state.finish,
|
finish,
|
||||||
state.reset,
|
reset,
|
||||||
]);
|
] = useStore((state) => [
|
||||||
|
state.state,
|
||||||
|
state.questions,
|
||||||
|
state.answers,
|
||||||
|
state.correctCount,
|
||||||
|
state.endTime,
|
||||||
|
state.load,
|
||||||
|
state.finish,
|
||||||
|
state.reset,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -101,18 +120,17 @@ export default function IzpitQuiz() {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<button
|
<div className="sticky inset-0 top-auto mb-10 flex select-none p-5">
|
||||||
className="button"
|
<div className="mx-auto flex items-center gap-6 rounded-lg border bg-white px-6 py-4 shadow-lg">
|
||||||
onClick={() =>
|
<div className="text-lg">
|
||||||
finish(
|
{answers!.filter(([v]) => v >= 0).length} / {answers!.length}
|
||||||
questions!
|
</div>
|
||||||
.map((q, qi) => q.correct === answers![qi][0])
|
<Countdown timeEnd={endTime!} />
|
||||||
.reduce((acc, cur) => acc + (cur ? 1 : 0), 0),
|
<button className="button text-sm" onClick={() => finish()}>
|
||||||
)
|
Zaključi
|
||||||
}
|
</button>
|
||||||
>
|
</div>
|
||||||
Zaključi
|
</div>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -156,3 +174,30 @@ export default function IzpitQuiz() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function Countdown({ timeEnd }: { timeEnd: Date }) {
|
||||||
|
const finish = useStore((state) => state.finish);
|
||||||
|
const [remaining, setRemaining] = useState(timeEnd.valueOf() - Date.now());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
const newVal = Math.max(0, timeEnd.valueOf() - Date.now());
|
||||||
|
setRemaining(newVal);
|
||||||
|
if (newVal === 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [timeEnd, finish]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="text-lg">
|
||||||
|
{Math.floor(remaining / 1000 / 60)}:
|
||||||
|
{Math.floor((remaining / 1000) % 60)
|
||||||
|
.toString()
|
||||||
|
.padStart(2, '0')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -3,19 +3,23 @@ import IzpitQuiz from './izpit-quiz';
|
|||||||
import { SubHeader } from '@/components/sub_header';
|
import { SubHeader } from '@/components/sub_header';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Simulator izpita',
|
title: 'Preizkusni izpit',
|
||||||
description: 'Pripomoček za simuliranje izpita za radioamaterski izpit',
|
description:
|
||||||
openGraph: {
|
'Rešite preizkusni radioamaterski izpit in preverite svoje znanje iz radioamaterstva',
|
||||||
title: 'Simulator izpita',
|
keywords: [
|
||||||
description: 'Pripomoček za simuliranje izpita za radioamaterski izpit',
|
'izpit',
|
||||||
},
|
'preizkus',
|
||||||
|
'preverjanje znanje',
|
||||||
|
'preizkusni izpit',
|
||||||
|
'radioamaterski izpit',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Priprave() {
|
export default function Priprave() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SubHeader>
|
<SubHeader>
|
||||||
<h1>Simulator izpita</h1>
|
<h1>Preizkusni izpit</h1>
|
||||||
<p>
|
<p>
|
||||||
Izpit je sestavljen iz <strong>60 različnih vprašanj</strong>. Vsako
|
Izpit je sestavljen iz <strong>60 različnih vprašanj</strong>. Vsako
|
||||||
vprašanje ima 3 možne odgovore, od katerih je samo en pravilen.
|
vprašanje ima 3 možne odgovore, od katerih je samo en pravilen.
|
||||||
|
@ -64,16 +64,16 @@ export default function CallsignTool() {
|
|||||||
<div className="flex flex-row gap-4">
|
<div className="flex flex-row gap-4">
|
||||||
<button
|
<button
|
||||||
onClick={() => setClas(0)}
|
onClick={() => setClas(0)}
|
||||||
className={`flex-1 rounded-lg border-4 bg-light py-2 font-semibold text-dark shadow ${
|
className={`flex-1 rounded-lg border-2 bg-light py-2 font-semibold text-dark ${
|
||||||
!clas ? 'border-dark' : 'border-transparent'
|
!clas ? 'border-primary bg-primary/20' : 'border-transparent'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
N razred
|
N razred
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setClas(1)}
|
onClick={() => setClas(1)}
|
||||||
className={`flex-1 rounded-lg border-4 bg-light py-2 font-semibold text-dark shadow ${
|
className={`flex-1 rounded-lg border-2 bg-light py-2 font-semibold text-dark ${
|
||||||
clas ? 'border-dark' : 'border-transparent'
|
clas ? 'border-primary bg-primary/20' : 'border-transparent'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
A razred
|
A razred
|
||||||
@ -85,7 +85,7 @@ export default function CallsignTool() {
|
|||||||
<label className="text-sm font-semibold">Vnesi klicni znak</label>
|
<label className="text-sm font-semibold">Vnesi klicni znak</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={`rounded-lg border border-light py-3 text-center text-3xl uppercase shadow-lg placeholder:font-sans placeholder:normal-case ${robotoMono.className}`}
|
className={`rounded-lg border border-gray-200 py-3 text-center text-3xl uppercase outline-primary placeholder:font-sans placeholder:normal-case ${robotoMono.className}`}
|
||||||
value={callsign}
|
value={callsign}
|
||||||
onChange={(e) => setCallsign(e.target.value)}
|
onChange={(e) => setCallsign(e.target.value)}
|
||||||
placeholder='npr. "S50HQ"'
|
placeholder='npr. "S50HQ"'
|
||||||
@ -153,23 +153,32 @@ export default function CallsignTool() {
|
|||||||
|
|
||||||
{showSimilar ? (
|
{showSimilar ? (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="mb-1 text-xl font-semibold">
|
<h4 className="mb-3 text-xl font-semibold">
|
||||||
Podobni prosti klicni znaki
|
Podobni prosti klicni znaki
|
||||||
</h4>
|
</h4>
|
||||||
<div
|
<div
|
||||||
className={`grid grid-cols-4 gap-2 md:grid-cols-5 ${robotoMono.className}`}
|
className={`grid grid-cols-4 gap-2 md:grid-cols-5 ${robotoMono.className}`}
|
||||||
>
|
>
|
||||||
{free
|
{free
|
||||||
?.map((c) => [levenshteinDistance(callsign, c), c])
|
?.map((c) => ({
|
||||||
.sort()
|
call: c,
|
||||||
|
diff: levenshteinDistance(callsign, c),
|
||||||
|
}))
|
||||||
|
.filter(
|
||||||
|
({ call }) => call.toLowerCase() !== callsign.toLowerCase(),
|
||||||
|
)
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.diff !== b.diff) return a.diff - b.diff;
|
||||||
|
return a.call.localeCompare(b.call);
|
||||||
|
})
|
||||||
.slice(0, 100)
|
.slice(0, 100)
|
||||||
.map((c) => (
|
.map(({ call }) => (
|
||||||
<button
|
<button
|
||||||
key={c[1]}
|
key={call}
|
||||||
onClick={() => setCallsign(c[1] as string)}
|
onClick={() => setCallsign(call)}
|
||||||
className="rounded-lg border bg-light p-1 text-center hover:border-dark"
|
className="rounded-lg border bg-light p-1 text-center hover:border-dark"
|
||||||
>
|
>
|
||||||
{c[1]}
|
{call}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,11 +4,13 @@ import CallsignTool from './callsign-tool';
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Izbira klicnega znaka',
|
title: 'Izbira klicnega znaka',
|
||||||
description: 'Pomoč pri izbiri klicnega znaka',
|
description: 'Orodje, ki vam pomaga pri izbiri lastnega klicnega znaka',
|
||||||
openGraph: {
|
keywords: [
|
||||||
title: 'Izbira klicnega znaka',
|
'klicni znak',
|
||||||
description: 'Pomoč pri izbiri klicnega znaka',
|
'callsign',
|
||||||
},
|
'call sign',
|
||||||
|
'radioamaterski klicni znak',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Callsign() {
|
export default function Callsign() {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Analytics } from '@vercel/analytics/react';
|
import { Analytics } from '@vercel/analytics/react';
|
||||||
import Header from '@/components/header';
|
import { Header } from '@/components/header';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import '@/styles/globals.scss';
|
import '@/styles/globals.scss';
|
||||||
import { inter, morse } from '@/fonts/fonts';
|
import { inter, morse } from '@/fonts/fonts';
|
||||||
@ -17,7 +17,27 @@ export const metadata: Metadata = {
|
|||||||
default: 'Radioamaterski izpit',
|
default: 'Radioamaterski izpit',
|
||||||
template: '%s | Radioamaterski izpit',
|
template: '%s | Radioamaterski izpit',
|
||||||
},
|
},
|
||||||
description: 'Priprava na radioamaterski izpit',
|
description:
|
||||||
|
'Vse o radioamaterskem izpitu, pripravi na izpit in pridobitev radioamaterskega dovoljenja in klicnega znaka',
|
||||||
|
keywords: [
|
||||||
|
'radioamater',
|
||||||
|
'izpit',
|
||||||
|
'radio',
|
||||||
|
'KV',
|
||||||
|
'HF',
|
||||||
|
'VHF',
|
||||||
|
'UKV',
|
||||||
|
'dovoljenje',
|
||||||
|
'licenca',
|
||||||
|
'CEPT',
|
||||||
|
'AKOS',
|
||||||
|
'ZRS',
|
||||||
|
'Zveza radioamaterjev Slovenije',
|
||||||
|
'radiotehnika',
|
||||||
|
'radioamaterstvo',
|
||||||
|
'Morzejeva abeceda',
|
||||||
|
'Morse kod',
|
||||||
|
],
|
||||||
icons: {
|
icons: {
|
||||||
icon: '/logo/icon_512.png',
|
icon: '/logo/icon_512.png',
|
||||||
shortcut: '/logo/icon_512.png',
|
shortcut: '/logo/icon_512.png',
|
||||||
@ -26,14 +46,9 @@ export const metadata: Metadata = {
|
|||||||
manifest: '/manifest.json',
|
manifest: '/manifest.json',
|
||||||
metadataBase: new URL('https://izpit.jkob.cc'),
|
metadataBase: new URL('https://izpit.jkob.cc'),
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: {
|
locale: 'sl',
|
||||||
default: 'Radioamaterski izpit',
|
|
||||||
template: '%s | Radioamaterski izpit',
|
|
||||||
},
|
|
||||||
description: 'Priprava na radioamaterski izpit',
|
|
||||||
url: 'https://izpit.jkob.cc/',
|
|
||||||
locale: 'sl_SL',
|
|
||||||
type: 'website',
|
type: 'website',
|
||||||
|
siteName: 'Radioamaterski tečaj',
|
||||||
},
|
},
|
||||||
robots: {
|
robots: {
|
||||||
index: true,
|
index: true,
|
||||||
|
@ -1,15 +1,22 @@
|
|||||||
import { SubHeader } from '@/components/sub_header';
|
import { SubHeader } from '@/components/sub_header';
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import RandomCallsign from './random_callsign';
|
import RandomCallsign from './random-callsign';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Radioamatersko dovoljenje',
|
title: 'Radioamatersko dovoljenje',
|
||||||
description: 'O radioamaterskem dovoljenju in klicnem znaku',
|
description:
|
||||||
openGraph: {
|
'Radioamatersko dovoljenje (CEPT licenca), ki vam omogoča, da začnete uporabljati radioamaterske frekvence',
|
||||||
title: 'Radioamatersko dovoljenje',
|
keywords: [
|
||||||
description: 'O radioamaterskem dovoljenju in klicnem znaku',
|
'dovoljenje',
|
||||||
},
|
'radioamatersko dovoljenje',
|
||||||
|
'licenca',
|
||||||
|
'CEPT licenca',
|
||||||
|
'radioamaterska licenca',
|
||||||
|
'klicni znak',
|
||||||
|
'callsign',
|
||||||
|
'call sign',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function License() {
|
export default function License() {
|
||||||
@ -82,7 +89,7 @@ export default function License() {
|
|||||||
Klicni znak si lahko izbereš glede na razred izpita, ki si ga opravil.
|
Klicni znak si lahko izbereš glede na razred izpita, ki si ga opravil.
|
||||||
</p>
|
</p>
|
||||||
<div className="my-6 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 px-4 shadow-md">
|
<div className="flex flex-1 flex-col rounded-lg bg-light px-4 shadow-md">
|
||||||
<h3 className="text-center">N razred</h3>
|
<h3 className="text-center">N razred</h3>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
@ -90,7 +97,7 @@ export default function License() {
|
|||||||
<li>S58AAA - S58XZZ</li>
|
<li>S58AAA - S58XZZ</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-1 flex-col gap-4 rounded-lg bg-light px-4 shadow-md">
|
<div className="flex flex-1 flex-col rounded-lg bg-light px-4 shadow-md">
|
||||||
<h3 className="text-center">A razred</h3>
|
<h3 className="text-center">A razred</h3>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -4,11 +4,9 @@ import { Metadata } from 'next';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
description: 'Informacije o radioamaterstvu in izpitu za pridobitev licence',
|
title: 'Radioamaterstvo, izpit in dovoljenje',
|
||||||
openGraph: {
|
description:
|
||||||
description:
|
'Izvedite kaj pomeni biti radioamater, kako začeti s hobijem, kako se pripraviti na izpit in kako pridobiti radioamatersko dovoljenje',
|
||||||
'Informacije o radioamaterstvu in izpitu za pridobitev licence',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const povezave = [
|
const povezave = [
|
||||||
@ -82,8 +80,13 @@ export default function Home() {
|
|||||||
vzpostavljanjem radijskih zvez, nekateri radi tekmujejo v
|
vzpostavljanjem radijskih zvez, nekateri radi tekmujejo v
|
||||||
vzpostavljanju radijskih zvez ali pa iskanjem skritih oddajnikov.
|
vzpostavljanju radijskih zvez ali pa iskanjem skritih oddajnikov.
|
||||||
Radioamaterji so tudi pomočniki v primeru naravnih nesreč, ko se
|
Radioamaterji so tudi pomočniki v primeru naravnih nesreč, ko se
|
||||||
porušijo komunikacijske povezave. Radioamaterji uporabljajo določene
|
porušijo komunikacijske povezave.
|
||||||
frekvence, ki so jim dodeljene s strani mednarodne organizacije ITU.
|
</p>
|
||||||
|
<p>
|
||||||
|
Radioamaterji uporabljajo določene frekvence, ki so jim dodeljene s
|
||||||
|
strani mednarodne organizacije ITU. Za uporabo teh frekvenc je
|
||||||
|
potrebno opraviti radioamaterski izpit in pridobiti radioamatersko
|
||||||
|
dovoljenje (CEPT licenco).
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -2,12 +2,17 @@ import { Metadata } from 'next';
|
|||||||
import VajaQuiz from './vaja-quiz';
|
import VajaQuiz from './vaja-quiz';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Priprave na izpit',
|
title: 'Vaja',
|
||||||
description: 'Naloge za pripravo na izpit',
|
description:
|
||||||
openGraph: {
|
'Pripravite se na radioamaterski izpit z vajo reševanja vprašanj, ki se lahko pojavijo na izpitu',
|
||||||
title: 'Priprave na izpit',
|
keywords: [
|
||||||
description: 'Naloge za pripravo na izpit',
|
'vaja za izpit',
|
||||||
},
|
'radioamaterske naloge',
|
||||||
|
'radioamaterske vaje',
|
||||||
|
'priprava na izpit',
|
||||||
|
'vprašanja za izpit',
|
||||||
|
'izpitna vprašanja',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Priprave() {
|
export default function Priprave() {
|
||||||
|
@ -1,15 +1,16 @@
|
|||||||
export default function sitemap() {
|
import { MetadataRoute } from 'next';
|
||||||
const pages = [
|
|
||||||
|
export default function sitemap(): MetadataRoute.Sitemap {
|
||||||
|
return [
|
||||||
'',
|
'',
|
||||||
'/licenca',
|
'/licenca',
|
||||||
'/klicni-znak',
|
'/klicni-znak',
|
||||||
'/zbirka',
|
'/zbirka',
|
||||||
'/priprave',
|
'/priprave',
|
||||||
'/izpit-sim',
|
'/izpit-sim',
|
||||||
].map((page) => ({
|
].map((page, index) => ({
|
||||||
url: `https://izpit.jkob.cc${page}`,
|
url: `https://izpit.jkob.cc${page}`,
|
||||||
lastModified: new Date().toISOString(),
|
lastModified: new Date().toISOString(),
|
||||||
|
priority: index === 0 ? 1 : 0.8,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return pages;
|
|
||||||
}
|
}
|
||||||
|
@ -4,11 +4,15 @@ import { SubHeader } from '@/components/sub_header';
|
|||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Zbirka vprašanj',
|
title: 'Zbirka vprašanj',
|
||||||
description: 'Zbirka vprašanj, ki se lahko pojavijo na izpitu.',
|
description:
|
||||||
openGraph: {
|
'Pregled cele zbirke vprašanj, ki se lahko pojavijo na radioamaterskem izpitu',
|
||||||
title: 'Zbirka vprašanj',
|
keywords: [
|
||||||
description: 'Zbirka vprašanj, ki se lahko pojavijo na izpitu.',
|
'vprašanja',
|
||||||
},
|
'naloge',
|
||||||
|
'radioamaterski izpit',
|
||||||
|
'zbirka vprašanj',
|
||||||
|
'zbirka nalog',
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function QuestionPool() {
|
export default function QuestionPool() {
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { faAngleDown } from '@fortawesome/free-solid-svg-icons';
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
const nav = [
|
const nav = [
|
||||||
{ href: '/', label: 'Domov' },
|
{ href: '/', label: 'Domov' },
|
||||||
@ -13,70 +11,83 @@ const nav = [
|
|||||||
{
|
{
|
||||||
label: 'Vaja',
|
label: 'Vaja',
|
||||||
children: [
|
children: [
|
||||||
{ href: '/zbirka', label: 'Zbirka' },
|
{ href: '/zbirka', label: 'Zbirka izpitnih vprašanj' },
|
||||||
{ href: '/priprave', label: 'Priprave' },
|
{ href: '/priprave', label: 'Vaja vprašanj' },
|
||||||
{ href: '/izpit-sim', label: 'Simulator izpita' },
|
{ href: '/izpit-sim', label: 'Preizkusni izpit' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// { href: "/izpit-gen", label: "Generator izpitnih pol" },
|
// { href: "/izpit-gen", label: "Generator izpitnih pol" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Header() {
|
export function Header() {
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMenuOpen(false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="select-none text-white">
|
<header className="relative flex select-none flex-col bg-darker font-medium text-white lg:flex-row lg:items-center [&>*]:z-30 [&>*]:bg-darker">
|
||||||
<div className="bg-darker py-6 font-bold">
|
<div className="flex flex-1 flex-row items-center justify-between">
|
||||||
<div className="container">
|
<div className="flex flex-row items-center justify-start gap-6 px-8 py-6">
|
||||||
<div className="flex flex-row items-center justify-start gap-8 px-4">
|
<Image src="/logo/logo_192.png" alt="Logo" height={24} width={24} />
|
||||||
<Image src="/logo/logo_192.png" alt="Logo" height={32} width={32} />
|
<div>
|
||||||
<div>
|
<Link href="/" className="text-2xl sm:text-3xl">
|
||||||
<Link href="/">
|
Radioamaterski izpit
|
||||||
<h1 className="text-3xl sm:text-4xl">Radioamaterski izpit</h1>
|
</Link>
|
||||||
</Link>
|
|
||||||
<div data-nosnippet className="morse mt-1 text-sm text-gray-400">
|
|
||||||
CQ|DE|HAMS
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="flex flex-col justify-between gap-2 px-6 lg:hidden [&>*]:h-0.5 [&>*]:w-6 [&>*]:bg-white [&>*]:transition-all"
|
||||||
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||||
|
title="Menu"
|
||||||
|
>
|
||||||
|
<div className={isMenuOpen ? 'translate-y-2.5 rotate-45' : ''} />
|
||||||
|
<div className={isMenuOpen ? 'opacity-0' : ''} />
|
||||||
|
<div className={isMenuOpen ? '-translate-y-2.5 -rotate-45' : ''} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="bg-dark">
|
<div
|
||||||
<nav className="container flex flex-row flex-wrap justify-start">
|
className={`absolute left-0 right-0 top-full flex flex-col items-stretch border-t border-primary lg:relative lg:flex-row lg:gap-4 lg:border-none lg:pr-10 ${isMenuOpen ? '' : 'hidden lg:flex'}`}
|
||||||
{nav.map(({ href, label, children }) =>
|
>
|
||||||
href ? (
|
{nav.map(({ href, label, children }) =>
|
||||||
<Link
|
href ? (
|
||||||
key={href}
|
<Link
|
||||||
href={href}
|
key={href}
|
||||||
className={`px-4 py-2 transition-colors ${
|
href={href}
|
||||||
href === pathname
|
className={`px-6 py-4 transition-colors lg:rounded-lg lg:px-4 lg:py-2 ${
|
||||||
? 'cursor-default bg-white/10'
|
href === pathname ? 'bg-dark' : 'hover:bg-dark active:bg-black'
|
||||||
: 'hover:bg-white/10 active:bg-white/30'
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
{label}
|
||||||
{label}
|
</Link>
|
||||||
</Link>
|
) : (
|
||||||
) : (
|
<Dropdown key={label} label={label}>
|
||||||
<Dropdown key={label} label={label}>
|
{children?.map(({ href, label }) => (
|
||||||
{children?.map(({ href, label }) => (
|
<Link
|
||||||
<Link
|
key={href}
|
||||||
key={href}
|
href={href}
|
||||||
href={href}
|
className={`px-6 py-4 transition-colors lg:rounded-lg lg:px-4 lg:py-2 ${
|
||||||
className={`px-4 py-2 transition-colors ${
|
href === pathname
|
||||||
href === pathname
|
? 'bg-dark'
|
||||||
? 'cursor-default bg-white/10'
|
: 'hover:bg-dark active:bg-black'
|
||||||
: 'hover:bg-white/10 active:bg-white/30'
|
}`}
|
||||||
}`}
|
>
|
||||||
>
|
{label}
|
||||||
{label}
|
</Link>
|
||||||
</Link>
|
))}
|
||||||
))}
|
</Dropdown>
|
||||||
</Dropdown>
|
),
|
||||||
),
|
)}
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={`fixed inset-0 !z-10 !bg-black/50 ${isMenuOpen ? 'block lg:hidden' : 'hidden'}`}
|
||||||
|
onClick={() => setIsMenuOpen(false)}
|
||||||
|
/>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -87,29 +98,30 @@ interface DropdownProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Dropdown({ label, children }: DropdownProps) {
|
function Dropdown({ label, children }: DropdownProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(0);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setOpen(0);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onMouseLeave={() => setOpen(false)} className="relative">
|
<div onMouseLeave={() => setOpen(0)} className="relative">
|
||||||
<button
|
<button
|
||||||
className={`flex flex-row items-center gap-2 px-4 py-2 transition-colors hover:bg-white/10 active:bg-white/30 ${
|
className={`hidden cursor-default px-6 py-4 transition-colors lg:block lg:cursor-auto lg:rounded-lg lg:px-4 lg:py-2 lg:hover:bg-dark lg:active:bg-black ${
|
||||||
open ? 'bg-white/10' : ''
|
open ? 'lg:bg-dark' : ''
|
||||||
}`}
|
}`}
|
||||||
onMouseOver={() => setOpen(true)}
|
onMouseOver={() => setOpen(open | 1)}
|
||||||
onClick={() => setOpen(!open)}
|
onClick={() => setOpen(open ^ 2)}
|
||||||
>
|
>
|
||||||
<span>{label}</span>
|
{label}
|
||||||
<FontAwesomeIcon icon={faAngleDown} className="pt-1" />
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{open && (
|
<div className={`right-0 lg:absolute lg:pt-2 ${open ? '' : 'lg:hidden'}`}>
|
||||||
<div
|
<div className="top-full z-10 flex flex-col border-dark shadow-lg lg:w-max lg:gap-2 lg:rounded-xl lg:border lg:bg-darker lg:p-4">
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
className="absolute left-0 top-full z-10 flex w-40 flex-col bg-darker shadow-lg"
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
15
src/components/lazy-tex.tsx
Normal file
15
src/components/lazy-tex.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { lazy } from 'react';
|
||||||
|
|
||||||
|
export const LazyTeX = lazy(() => import('./tex'));
|
||||||
|
|
||||||
|
export function MaybeTeX({ text }: { text: string }) {
|
||||||
|
const parts = text.split(/(?<!\\)\$+/);
|
||||||
|
|
||||||
|
return parts.map((part, i) =>
|
||||||
|
!part ? null : i % 2 === 0 ? (
|
||||||
|
<span key={i}>{part}</span>
|
||||||
|
) : (
|
||||||
|
<LazyTeX key={i} math={part} />
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
@ -1,12 +1,11 @@
|
|||||||
import { Question } from '@/interfaces/question';
|
import { Question } from '@/interfaces/question';
|
||||||
import { InlineMath } from 'react-katex';
|
import { MaybeTeX } from './lazy-tex';
|
||||||
import styles from '@/styles/Quiz.module.scss';
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
interface QuestionCardProps {
|
interface QuestionCardProps {
|
||||||
question: Question;
|
question: Question;
|
||||||
reveal: boolean;
|
reveal: boolean;
|
||||||
selected: number[];
|
selected: number[] | number;
|
||||||
onClick?: (answer: number) => void;
|
onClick?: (answer: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -19,19 +18,21 @@ export default function QuestionCard({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<div className="text-xl text-gray-700">
|
<div className="text-xl text-gray-700">
|
||||||
<span className="font-bold text-primary">
|
<span className="font-medium text-primary">
|
||||||
A{question.id.toString().padStart(3, '0')}:{' '}
|
<span className="text-sm">#</span>
|
||||||
|
{question.id.toString().padStart(3, '0')}:{' '}
|
||||||
</span>
|
</span>
|
||||||
<MaybeTeX text={question.question} />
|
<MaybeTeX text={question.question} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{question.image && (
|
{question.image && (
|
||||||
<Image
|
<Image
|
||||||
className={styles.image}
|
className="max-h-80 max-w-full object-contain"
|
||||||
src={`/question_images/${question.image}`}
|
src={`/question_images/${question.image}`}
|
||||||
alt={question.image}
|
alt={question.image}
|
||||||
height={500}
|
height={500}
|
||||||
width={500}
|
width={500}
|
||||||
|
style={{ width: '100%', height: 'auto' }}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -43,7 +44,9 @@ export default function QuestionCard({
|
|||||||
answer={answer}
|
answer={answer}
|
||||||
reveal={reveal}
|
reveal={reveal}
|
||||||
isCorrect={question.correct === i}
|
isCorrect={question.correct === i}
|
||||||
isSelected={selected.includes(i)}
|
isSelected={
|
||||||
|
selected instanceof Array ? selected.includes(i) : selected === i
|
||||||
|
}
|
||||||
onClick={!onClick ? undefined : () => onClick(i)}
|
onClick={!onClick ? undefined : () => onClick(i)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
@ -71,41 +74,21 @@ function Answer({
|
|||||||
}: AnswerProps) {
|
}: AnswerProps) {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`flex w-full flex-row items-center gap-5 rounded border px-6 py-2 ${
|
className={`flex h-auto flex-nowrap items-center justify-start gap-6 rounded-lg px-6 py-1 font-normal normal-case ${
|
||||||
!isSelected
|
!isSelected
|
||||||
? 'border-gray-300'
|
? 'bg-light'
|
||||||
: !reveal
|
: !reveal
|
||||||
? 'border-sky-500 bg-sky-100'
|
? 'bg-primary/30'
|
||||||
: isCorrect
|
: isCorrect
|
||||||
? 'border-green-500 bg-green-100'
|
? 'bg-green-200 text-green-950'
|
||||||
: 'border-red-600 bg-red-100'
|
: 'bg-red-200 text-red-950'
|
||||||
}`}
|
} ${!onClick || isSelected ? 'cursor-default' : 'hover:bg-primary/10'}`}
|
||||||
disabled={!onClick}
|
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
{!reveal && <input type="radio" checked={isSelected} readOnly />}
|
<div className="text-sm font-bold">{String.fromCharCode(65 + index)}</div>
|
||||||
<div
|
<div className="py-2 text-left text-base">
|
||||||
className={`border-r py-2 pr-5 text-sm font-bold ${
|
|
||||||
!isSelected ? 'border-gray-200' : 'border-inherit'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{String.fromCharCode(65 + index)}
|
|
||||||
</div>
|
|
||||||
<div className="text-left text-lg text-gray-600">
|
|
||||||
<MaybeTeX text={answer} />
|
<MaybeTeX text={answer} />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MaybeTeX({ text }: { text: string }) {
|
|
||||||
const parts = text.split(/(?<!\\)\$+/);
|
|
||||||
|
|
||||||
return parts.map((part, i) =>
|
|
||||||
!part ? null : i % 2 === 0 ? (
|
|
||||||
<span key={i}>{part}</span>
|
|
||||||
) : (
|
|
||||||
<InlineMath key={i} math={part} />
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
15
src/components/tex.tsx
Normal file
15
src/components/tex.tsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import 'katex/dist/katex.min.css';
|
||||||
|
import KaTeX from 'katex';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
interface TeXProps {
|
||||||
|
math: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(TeX);
|
||||||
|
|
||||||
|
export function TeX({ math }: TeXProps) {
|
||||||
|
const innerHtml = KaTeX.renderToString(math);
|
||||||
|
|
||||||
|
return <span dangerouslySetInnerHTML={{ __html: innerHtml }} />;
|
||||||
|
}
|
@ -1,14 +0,0 @@
|
|||||||
.image {
|
|
||||||
max-width: 100% important;
|
|
||||||
max-height: 20rem !important;
|
|
||||||
height: auto !important;
|
|
||||||
margin: auto !important;
|
|
||||||
object-fit: contain !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.answer {
|
|
||||||
justify-content: start !important;
|
|
||||||
text-align: start !important;
|
|
||||||
height: auto !important;
|
|
||||||
white-space: normal !important;
|
|
||||||
}
|
|
@ -5,7 +5,7 @@
|
|||||||
@import 'katex/dist/katex.min.css';
|
@import 'katex/dist/katex.min.css';
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
@apply flex flex-row items-center justify-center rounded-lg bg-primary px-5 py-2 font-semibold text-white decoration-transparent shadow-lg transition-colors hover:bg-primary-dark active:bg-primary-darker disabled:opacity-70;
|
@apply flex flex-row items-center justify-center rounded-lg bg-primary px-5 py-2 font-semibold text-white decoration-transparent transition-colors hover:bg-primary-dark active:bg-primary-darker disabled:opacity-70;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
.link {
|
||||||
@ -17,8 +17,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
padding-top: 3rem;
|
@apply py-12;
|
||||||
padding-bottom: 3rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main > :not(.section):last-child {
|
main > :not(.section):last-child {
|
||||||
@ -33,16 +32,10 @@ main > .section:last-child {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose {
|
.prose {
|
||||||
h1 {
|
h1,
|
||||||
@apply text-3xl font-semibold;
|
h2,
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
@apply text-2xl font-semibold;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
@apply text-xl font-semibold;
|
@apply font-semibold;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
@ -44,34 +44,43 @@ export function generateAllCallsigns({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function levenshteinDistance(from: string, to: string): number {
|
export function levenshteinDistance(from: string, to: string): number {
|
||||||
const addWeight = 5;
|
const addWeight = 1.6;
|
||||||
const removeWeight = 0;
|
const removeWeight = 0.4;
|
||||||
const replaceWeight = 1;
|
const replaceWeight = 1;
|
||||||
|
const swapWeight = 0.5;
|
||||||
|
|
||||||
from = from.toUpperCase();
|
from = from.toUpperCase();
|
||||||
to = to.toUpperCase();
|
to = to.toUpperCase();
|
||||||
|
|
||||||
let prev: number[] = [];
|
const table: number[][] = [[]];
|
||||||
for (let i = 0; i < to.length + 1; i++) prev[i] = i;
|
for (let i = 0; i < to.length + 1; i++) table[0][i] = i * addWeight;
|
||||||
|
|
||||||
for (let i = 1; i < from.length + 1; i++) {
|
for (let i = 1; i < from.length + 1; i++) {
|
||||||
const curr: number[] = [];
|
const curr: number[] = [];
|
||||||
curr[0] = i;
|
curr[0] = i * removeWeight;
|
||||||
|
|
||||||
for (let j = 1; j < to.length + 1; j++) {
|
for (let j = 1; j < to.length + 1; j++) {
|
||||||
const cost =
|
const cost =
|
||||||
from[i - 1] === to[j - 1] || from[i - 1] === '*' ? 0 : replaceWeight;
|
from[i - 1] === to[j - 1] || from[i - 1] === '*' ? 0 : replaceWeight;
|
||||||
curr[j] = Math.min(
|
curr[j] = Math.min(
|
||||||
prev[j] + removeWeight,
|
table[i - 1][j] + removeWeight,
|
||||||
curr[j - 1] + addWeight,
|
curr[j - 1] + addWeight,
|
||||||
prev[j - 1] + cost,
|
table[i - 1][j - 1] + cost,
|
||||||
);
|
);
|
||||||
|
if (
|
||||||
|
i > 1 &&
|
||||||
|
j > 1 &&
|
||||||
|
from[i - 2] === to[j - 1] &&
|
||||||
|
from[i - 1] === to[j - 2]
|
||||||
|
) {
|
||||||
|
curr[j] = Math.min(curr[j], table[i - 2][j - 2] + swapWeight);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prev = curr;
|
table.push(curr);
|
||||||
}
|
}
|
||||||
|
|
||||||
return prev[to.length];
|
return table[from.length][to.length];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function cwWeight(text: string): number {
|
export function cwWeight(text: string): number {
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
|
||||||
module.exports = {
|
|
||||||
content: [
|
|
||||||
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
|
||||||
],
|
|
||||||
theme: {
|
|
||||||
extend: {
|
|
||||||
colors: {
|
|
||||||
darker: "#222831",
|
|
||||||
dark: "#393E46",
|
|
||||||
primary: {
|
|
||||||
light: "#00F4FF",
|
|
||||||
DEFAULT: "#00ADB5",
|
|
||||||
dark: "#008288",
|
|
||||||
darker: "#00656A",
|
|
||||||
},
|
|
||||||
light: "#EEEEEE",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
container: {
|
|
||||||
padding: '2rem',
|
|
||||||
center: true,
|
|
||||||
screens: {
|
|
||||||
DEFAULT: '100%',
|
|
||||||
sm: '640px',
|
|
||||||
md: '768px',
|
|
||||||
lg: '1024px',
|
|
||||||
xl: '1280px',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
require('@tailwindcss/typography'),
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
36
tailwind.config.ts
Normal file
36
tailwind.config.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import defaultTheme from 'tailwindcss/defaultTheme';
|
||||||
|
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
darker: '#222831',
|
||||||
|
dark: '#393E46',
|
||||||
|
primary: {
|
||||||
|
light: '#00F4FF',
|
||||||
|
DEFAULT: '#00ADB5',
|
||||||
|
dark: '#008288',
|
||||||
|
darker: '#00656A',
|
||||||
|
},
|
||||||
|
light: '#F8FAFA',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
padding: '2rem',
|
||||||
|
center: true,
|
||||||
|
screens: {
|
||||||
|
DEFAULT: '100%',
|
||||||
|
sm: '640px',
|
||||||
|
md: '768px',
|
||||||
|
lg: '1024px',
|
||||||
|
xl: '1280px',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['Inter', ...defaultTheme.fontFamily.sans],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [require('@tailwindcss/typography')],
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user