This commit is contained in:
2025-06-19 21:39:32 +02:00
parent 8d12648e3a
commit a0d9bfd5dc
34 changed files with 1645 additions and 7410 deletions
+60 -60
View File
@@ -1,73 +1,73 @@
import { Button } from '@nextui-org/react'
import React, { useState, useEffect, useCallback } from 'react'
import type { ButtonProps } from '@/utils/props'
import type { ButtonColor } from '@/utils/types'
import { Button } from "@heroui/react";
import React, { useCallback, useEffect, useState } from "react";
import type { ButtonProps } from "@/utils/props";
import type { ButtonColor } from "@/utils/types";
const CustomButton: React.FC<ButtonProps> = React.memo(({ label, link }) => {
const [status, setStatus] = useState<number>()
const [isLoading, setIsLoading] = useState<boolean>(false)
const [status, setStatus] = useState<number>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const checkLinkStatus = useCallback(async (): Promise<void> => {
if (link) {
try {
setIsLoading(true)
const response = await fetch(`/api/validate?link=${encodeURI(link)}`)
const data = (await response.json()) as { status: number }
setStatus(data.status)
} catch (error) {
setStatus(500)
} finally {
setIsLoading(false)
}
}
}, [link])
const checkLinkStatus = useCallback(async (): Promise<void> => {
if (link) {
try {
setIsLoading(true);
const response = await fetch(`/api/validate?link=${encodeURI(link)}`);
const data = (await response.json()) as { status: number };
setStatus(data.status);
} catch (_error) {
setStatus(500);
} finally {
setIsLoading(false);
}
}
}, [link]);
useEffect(() => {
void checkLinkStatus()
}, [checkLinkStatus])
useEffect(() => {
void checkLinkStatus();
}, [checkLinkStatus]);
const getColor = useCallback((): ButtonColor => {
switch (true) {
case isLoading:
return 'default'
case status === 200:
return 'primary'
case status === 404:
return 'danger'
default:
return 'default'
}
}, [isLoading, status])
const getColor = useCallback((): ButtonColor => {
switch (true) {
case isLoading:
return "default";
case status === 200:
return "primary";
case status === 404:
return "danger";
default:
return "default";
}
}, [isLoading, status]);
const handleClick = useCallback(() => {
if (status === 200 && link) {
window.open(link)
} else {
console.error('A hivatkozás nem elérhető.')
}
}, [status, link])
const handleClick = useCallback(() => {
if (status === 200 && link) {
window.open(link);
} else {
console.error("A hivatkozás nem elérhető.");
}
}, [status, link]);
return (
<Button
isDisabled={status !== 200 || !link || isLoading}
isLoading={isLoading}
className='w-28 mt-3 text-sm font-bold py-2 px-2'
color={getColor()}
onClick={handleClick}
>
{label}
</Button>
)
})
return (
<Button
isDisabled={status !== 200 || !link || isLoading}
isLoading={isLoading}
className="w-28 mt-3 text-sm font-bold py-2 px-2"
color={getColor()}
onPress={handleClick}
>
{label}
</Button>
);
});
export const PdfButton: React.FC<ButtonProps> = React.memo(
({ label, link }) => <CustomButton label={label} link={link} />
)
({ label, link }) => <CustomButton label={label} link={link} />,
);
export const ZipButton: React.FC<ButtonProps> = React.memo(
({ label, link }) => <CustomButton label={label} link={link} />
)
({ label, link }) => <CustomButton label={label} link={link} />,
);
export const Mp3Button: React.FC<ButtonProps> = React.memo(
({ label, link }) => <CustomButton label={label} link={link} />
)
({ label, link }) => <CustomButton label={label} link={link} />,
);
+9 -9
View File
@@ -1,11 +1,11 @@
import { ThemeSwitcher } from '@/components/ThemeSwitcher'
import { Source } from '@/components/Source'
import { Source } from "@/components/Source";
import { ThemeSwitcher } from "@/components/ThemeSwitcher";
export const Footer = () => {
return (
<div className='fixed bottom-0 py-5 left-0 right-0 text-center space-x-5'>
<Source />
<ThemeSwitcher />
</div>
)
}
return (
<div className="fixed bottom-0 py-5 left-0 right-0 text-center space-x-5">
<Source />
<ThemeSwitcher />
</div>
);
};
+65 -68
View File
@@ -1,80 +1,77 @@
import { Select, SelectItem } from '@nextui-org/react'
import type { SelectorProps } from '@/utils/props'
import { Select, SelectItem } from "@heroui/react";
import type { SelectorProps } from "@/utils/props";
import type { ChangeEvent } from "react";
export const SubjectSelector: React.FC<
Pick<SelectorProps, 'selectedSubject' | 'setSelectedSubject' | 'subjects'>
Pick<SelectorProps, "selectedSubject" | "setSelectedSubject" | "subjects">
> = ({ selectedSubject, setSelectedSubject, subjects }) => (
<Select
selectionMode='single'
disallowEmptySelection={true}
label='Tárgy'
value={selectedSubject}
onChange={(e) => setSelectedSubject(e.target.value)}
className='w-56'
>
{subjects.map((subject) => (
<SelectItem key={subject.value} value={subject.value}>
{subject.label}
</SelectItem>
))}
</Select>
)
<Select
selectionMode="single"
disallowEmptySelection={true}
label="Tárgy"
value={selectedSubject}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
setSelectedSubject(e.target.value)
}
className="w-56"
>
{subjects.map((subject) => (
<SelectItem key={subject.value}>{subject.label}</SelectItem>
))}
</Select>
);
export const YearSelector: React.FC<
Pick<SelectorProps, 'selectedYear' | 'setSelectedYear' | 'years'>
Pick<SelectorProps, "selectedYear" | "setSelectedYear" | "years">
> = ({ selectedYear, setSelectedYear, years }) => (
<Select
selectionMode='single'
disallowEmptySelection={true}
label='Év'
value={selectedYear}
onChange={(e) => setSelectedYear(e.target.value)}
className='w-56'
>
{years.map((year) => (
<SelectItem key={year} value={year}>
{year}
</SelectItem>
))}
</Select>
)
<Select
selectionMode="single"
disallowEmptySelection={true}
label="Év"
value={selectedYear}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
setSelectedYear(e.target.value)
}
className="w-56"
>
{years.map((year) => (
<SelectItem key={year}>{year}</SelectItem>
))}
</Select>
);
export const PeriodSelector: React.FC<
Pick<SelectorProps, 'selectedPeriod' | 'setSelectedPeriod'>
Pick<SelectorProps, "selectedPeriod" | "setSelectedPeriod">
> = ({ selectedPeriod, setSelectedPeriod }) => (
<Select
selectionMode='single'
disallowEmptySelection={true}
label='Időszak'
value={selectedPeriod}
onChange={(e) => setSelectedPeriod(e.target.value)}
className='w-56'
>
<SelectItem key={'tavasz'} value={'tavasz'}>
Tavasz
</SelectItem>
<SelectItem key={'osz'} value={'osz'}>
Ősz
</SelectItem>
</Select>
)
<Select
selectionMode="single"
disallowEmptySelection={true}
label="Időszak"
value={selectedPeriod}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
setSelectedPeriod(e.target.value)
}
className="w-56"
>
<SelectItem key={"tavasz"}>Tavasz</SelectItem>
<SelectItem key={"osz"}>Ősz</SelectItem>
</Select>
);
export const LevelSelector: React.FC<
Pick<SelectorProps, 'selectedLevel' | 'setSelectedLevel'>
Pick<SelectorProps, "selectedLevel" | "setSelectedLevel">
> = ({ selectedLevel, setSelectedLevel }) => (
<Select
selectionMode='single'
disallowEmptySelection={true}
label='Szint'
value={selectedLevel}
onChange={(e) => setSelectedLevel(e.target.value)}
className='w-56'
>
<SelectItem key={'kozep'} value={'kozep'}>
Közép
</SelectItem>
<SelectItem key={'emelt'} value={'emelt'}>
Emelt
</SelectItem>
</Select>
)
<Select
selectionMode="single"
disallowEmptySelection={true}
label="Szint"
value={selectedLevel}
onChange={(e: ChangeEvent<HTMLSelectElement>) =>
setSelectedLevel(e.target.value)
}
className="w-56"
>
<SelectItem key={"kozep"}>Közép</SelectItem>
<SelectItem key={"emelt"}>Emelt</SelectItem>
</Select>
);
+14 -14
View File
@@ -1,16 +1,16 @@
import { VscGithubInverted } from 'react-icons/vsc'
import { Button } from '@nextui-org/button'
import { Button } from "@heroui/button";
import { VscGithubInverted } from "react-icons/vsc";
export const Source = () => {
return (
<Button
aria-label='Source Code'
size='sm'
onClick={() =>
window.open('https://github.com/skidoodle/erettsegi-browser')
}
>
<VscGithubInverted size={20} />
</Button>
)
}
return (
<Button
aria-label="Source Code"
size="sm"
onPress={() =>
window.open("https://github.com/skidoodle/erettsegi-browser")
}
>
<VscGithubInverted size={20} />
</Button>
);
};
+17 -17
View File
@@ -1,21 +1,21 @@
import { VscColorMode } from 'react-icons/vsc'
import { Button } from '@nextui-org/button'
import { useTheme } from 'next-themes'
import { Button } from "@heroui/button";
import { useTheme } from "next-themes";
import { VscColorMode } from "react-icons/vsc";
export const ThemeSwitcher = () => {
const { theme, setTheme } = useTheme()
const { theme, setTheme } = useTheme();
const toggle = () => {
setTheme(theme === 'light' ? 'dark' : 'light')
}
const toggle = () => {
setTheme(theme === "light" ? "dark" : "light");
};
return (
<Button aria-label='Switch Theme' size='sm' onClick={() => toggle()}>
{theme === 'light' ? (
<VscColorMode style={{ fill: 'black' }} size={20} />
) : (
<VscColorMode size={20} key={'dark'} />
)}
</Button>
)
}
return (
<Button aria-label="Switch Theme" size="sm" onPress={() => toggle()}>
{theme === "light" ? (
<VscColorMode style={{ fill: "black" }} size={20} />
) : (
<VscColorMode size={20} key={"dark"} />
)}
</Button>
);
};
+36 -36
View File
@@ -1,40 +1,40 @@
import { useState } from 'react'
import useYears from '@/hooks/useYears'
import { useState } from "react";
import useYears from "@/hooks/useYears";
export const useAppState = () => {
const [flPdfLink, setflPdfLink] = useState<string>('')
const [utPdfLink, setutPdfLink] = useState<string>('')
const [flZipLink, setflZipLink] = useState<string>('')
const [utZipLink, setutZipLink] = useState<string>('')
const [flMp3Link, setflMp3Link] = useState<string>('')
const [selectedSubject, setSelectedSubject] = useState<string>('')
const [selectedYear, setSelectedYear] = useState<string>('')
const [selectedPeriod, setSelectedPeriod] = useState<string>('')
const [selectedLevel, setSelectedLevel] = useState<string>('')
const [years, setYears] = useState<string[]>([])
const [flPdfLink, setflPdfLink] = useState<string>("");
const [utPdfLink, setutPdfLink] = useState<string>("");
const [flZipLink, setflZipLink] = useState<string>("");
const [utZipLink, setutZipLink] = useState<string>("");
const [flMp3Link, setflMp3Link] = useState<string>("");
const [selectedSubject, setSelectedSubject] = useState<string>("");
const [selectedYear, setSelectedYear] = useState<string>("");
const [selectedPeriod, setSelectedPeriod] = useState<string>("");
const [selectedLevel, setSelectedLevel] = useState<string>("");
const [years, setYears] = useState<string[]>([]);
useYears(setYears)
useYears(setYears);
return {
flPdfLink,
setflPdfLink,
utPdfLink,
setutPdfLink,
flZipLink,
setflZipLink,
utZipLink,
setutZipLink,
flMp3Link,
setflMp3Link,
selectedSubject,
setSelectedSubject,
selectedYear,
setSelectedYear,
selectedPeriod,
setSelectedPeriod,
selectedLevel,
setSelectedLevel,
years,
setYears,
}
}
return {
flPdfLink,
setflPdfLink,
utPdfLink,
setutPdfLink,
flZipLink,
setflZipLink,
utZipLink,
setutZipLink,
flMp3Link,
setflMp3Link,
selectedSubject,
setSelectedSubject,
selectedYear,
setSelectedYear,
selectedPeriod,
setSelectedPeriod,
selectedLevel,
setSelectedLevel,
years,
setYears,
};
};
+10 -10
View File
@@ -1,14 +1,14 @@
import { useEffect } from 'react'
import { useEffect } from "react";
export default function useYears(
setYears: React.Dispatch<React.SetStateAction<string[]>>
setYears: React.Dispatch<React.SetStateAction<string[]>>,
) {
useEffect(() => {
const currentYear = new Date().getFullYear()
const availableYears: string[] = []
for (let year = currentYear; year >= 2013; year--) {
availableYears.push(year.toString())
}
setYears(availableYears)
}, [setYears])
useEffect(() => {
const currentYear = new Date().getFullYear();
const availableYears: string[] = [];
for (let year = currentYear; year >= 2013; year--) {
availableYears.push(year.toString());
}
setYears(availableYears);
}, [setYears]);
}
+38
View File
@@ -0,0 +1,38 @@
import { Button } from "@heroui/button";
import { useRouter } from "next/router";
import { Footer } from "@/components/Footer";
export default function ErrorPage() {
const router = useRouter();
const handleBack = () => {
router.push("/");
};
return (
<>
<main className="dark:bg-[#121212] text-foreground bg-background py-5">
<h1 className="text-7xl font-bold text-blue-400 text-center mt-16">
404
</h1>
<div className="flex min-h-screen flex-col items-center justify-between">
<div className="container mx-auto">
<div className="flex flex-col items-center justify-center">
<div className="mt-5 mb-3">
<div className="text-2xl font-semibold text-gray-600">
<p className="mt-2">Az keresett oldal nem található.</p>
<p className="mt-8 text-center">
<Button color="primary" onPress={handleBack}>
Vissza
</Button>
</p>
</div>
</div>
</div>
</div>
</div>
<Footer />
</main>
</>
);
}
+25 -27
View File
@@ -1,32 +1,30 @@
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { Analytics } from '@vercel/analytics/react'
import { NextUIProvider } from '@nextui-org/react'
import { Inter } from 'next/font/google'
import type { AppProps } from 'next/app'
import Head from 'next/head'
import '@/styles/globals.css'
import { HeroUIProvider } from "@heroui/react";
import type { AppProps } from "next/app";
import { Inter } from "next/font/google";
import Head from "next/head";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import "@/styles/globals.css";
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
subsets: ["latin"],
variable: "--font-inter",
});
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Analytics />
<Head>
<title>Érettségi kereső</title>
</Head>
<NextUIProvider className={`${inter.variable} font-sans`}>
<NextThemesProvider
attribute='class'
defaultTheme='dark'
enableSystem={true}
>
<Component {...pageProps} />
</NextThemesProvider>
</NextUIProvider>
</>
)
return (
<>
<Head>
<title>Érettségi kereső</title>
</Head>
<HeroUIProvider className={`${inter.variable} font-sans`}>
<NextThemesProvider
attribute="class"
defaultTheme="dark"
enableSystem={true}
>
<Component {...pageProps} />
</NextThemesProvider>
</HeroUIProvider>
</>
);
}
+32 -32
View File
@@ -1,35 +1,35 @@
import { Html, Head, Main, NextScript } from 'next/document'
import { Head, Html, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang='hu'>
<Head>
<meta name='theme-color' content='#121212' />
<meta name='title' content='Érettségi kereső' />
<meta name='og:title' content='Érettségi kereső' />
<meta property='og:url' content='https://erettsegi.albert.lol' />
<meta
name='description'
content='Egyszerű keresés és letöltés az érettségi feladatsorokhoz. 🏫'
/>
<meta
name='og:description'
content='Egyszerű keresés és letöltés az érettségi feladatsorokhoz. 🏫'
/>
<script
defer
src="https://analytics.albert.lol/script.js"
data-website-id="7b196f47-39c9-4b8e-8dfd-b6e707282eea">
</script>
<link rel='icon' href='/favicon.ico' />
<meta property='image' content='/logo.png' />
<meta property='og:image' content='/logo.png' />
<meta name='author' content='albert' />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
return (
<Html lang="hu">
<Head>
<meta name="theme-color" content="#121212" />
<meta name="title" content="Érettségi kereső" />
<meta name="og:title" content="Érettségi kereső" />
<meta property="og:url" content="https://erettsegi.albert.lol" />
<meta
name="description"
content="Egyszerű keresés és letöltés az érettségi feladatsorokhoz. 🏫"
/>
<meta
name="og:description"
content="Egyszerű keresés és letöltés az érettségi feladatsorokhoz. 🏫"
/>
<script
defer
src="https://analytics.albert.lol/script.js"
data-website-id="7b196f47-39c9-4b8e-8dfd-b6e707282eea"
></script>
<link rel="icon" href="/favicon.ico" />
<meta property="image" content="/logo.png" />
<meta property="og:image" content="/logo.png" />
<meta name="author" content="albert" />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
);
}
-70
View File
@@ -1,70 +0,0 @@
import { Footer } from '@/components/Footer'
import { Button } from '@nextui-org/button'
import type { GetServerSideProps, GetServerSidePropsContext } from 'next'
interface ErrorProps {
statusCode: number
}
const NotFound: React.FC = () => (
<>
<p className='mt-2'>Az keresett oldal nem található.</p>
<p className='mt-8 text-center'>
<Button color='primary' onPress={() => (window.location.href = '/')}>
Vissza
</Button>
</p>
</>
)
const Unexpected: React.FC = () => (
<>
<p className='mt-2'>Váratlan hiba történt.</p>
<p className='mt-8 text-center'>
<Button color='primary' onPress={() => (window.location.href = '/')}>
Vissza
</Button>
</p>
</>
)
const ErrorPage: React.FC<ErrorProps> = ({ statusCode }) => {
return (
<>
<main className='dark:bg-[#121212] text-foreground bg-background py-5'>
<h1 className='text-7xl font-bold text-blue-400 text-center mt-16'>
{statusCode}
</h1>
<div className='flex min-h-screen flex-col items-center justify-between'>
<div className='container mx-auto'>
<div className='flex flex-col items-center justify-center'>
<div className='mt-5 mb-3'>
<div className='text-2xl font-semibold text-gray-600'>
{(() => {
switch (statusCode) {
case 404:
return <NotFound />
default:
return <Unexpected />
}
})()}
</div>
</div>
</div>
</div>
</div>
<Footer />
</main>
</>
)
}
export const getServerSideProps: GetServerSideProps<ErrorProps> = async (
context: GetServerSidePropsContext
) => ({
props: {
statusCode: context.res ? context.res.statusCode : 404,
},
})
export default ErrorPage
+108 -86
View File
@@ -1,100 +1,122 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import { subjects } from '@/utils/subjects'
import type { NextApiRequest, NextApiResponse } from "next";
import { subjects } from "@/utils/subjects";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { vizsgatargy, ev, idoszak, szint } = req.query as {
vizsgatargy: string
ev: string
idoszak: string
szint: string
}
try {
const { vizsgatargy, ev, idoszak, szint } = req.query as {
vizsgatargy: string;
ev: string;
idoszak: string;
szint: string;
};
const secure = req.headers['x-forwarded-proto'] === 'https'
const protocol = secure ? 'https' : 'http'
const address = req.headers.host
const secure = req.headers["x-forwarded-proto"] === "https";
const protocol = secure ? "https" : "http";
const address = req.headers.host;
const baseUrl = `https://dload-oktatas.educatio.hu/erettsegi/feladatok_${ev}${idoszak}_${szint}/`
const proxiedUrl = `${protocol}://${address}/api/proxy?link=${encodeURI(
baseUrl
)}`
const baseUrl = `https://dload-oktatas.educatio.hu/erettsegi/feladatok_${ev}${idoszak}_${szint}/`;
const proxiedUrl = `${protocol}://${address}/api/proxy?link=${encodeURI(
baseUrl,
)}`;
const missingParams = []
if (!ev) missingParams.push('ev')
if (!szint) missingParams.push('szint')
if (!idoszak) missingParams.push('idoszak')
if (!vizsgatargy) missingParams.push('vizsgatargy')
const missingParams = [];
if (!ev) missingParams.push("ev");
if (!szint) missingParams.push("szint");
if (!idoszak) missingParams.push("idoszak");
if (!vizsgatargy) missingParams.push("vizsgatargy");
if (missingParams.length > 0) {
return res
.status(400)
.json({ error: `Hiányzó paraméterek: ${missingParams.join(', ')}` })
}
if (missingParams.length > 0) {
return res
.status(400)
.json({ error: `Hiányzó paraméterek: ${missingParams.join(", ")}` });
}
if (ev <= '2012') {
return res.status(400).json({ error: 'Érvénytelen év' })
}
if (ev <= "2012") {
return res.status(400).json({ error: "Érvénytelen év" });
}
const validSubjects = subjects.map((subject) => subject.value)
if (!vizsgatargy || !validSubjects.includes(vizsgatargy)) {
return res.status(400).json({ error: 'Érvénytelen vizsgatárgy' })
}
const validSubjects = subjects.map((subject) => subject.value);
if (!vizsgatargy || !validSubjects.includes(vizsgatargy)) {
return res.status(400).json({ error: "Érvénytelen vizsgatárgy" });
}
let honap: string
switch (idoszak) {
case 'osz':
honap = 'okt'
break
case 'tavasz':
honap = 'maj'
break
default:
return res.status(400).json({ error: 'Érvénytelen időszak' })
}
let honap: string;
switch (idoszak) {
case "osz":
honap = "okt";
break;
case "tavasz":
honap = "maj";
break;
default:
return res.status(400).json({ error: "Érvénytelen időszak" });
}
let prefix: string
switch (szint) {
case 'emelt':
prefix = `e_${vizsgatargy}`
break
case 'kozep':
prefix = `k_${vizsgatargy}`
break
default:
return res.status(400).json({ error: 'Érvénytelen szint' })
}
let prefix: string;
switch (szint) {
case "emelt":
prefix = `e_${vizsgatargy}`;
break;
case "kozep":
prefix = `k_${vizsgatargy}`;
break;
default:
return res.status(400).json({ error: "Érvénytelen szint" });
}
const feladat = 'fl'
const utmutato = 'ut'
const forras = 'for'
const megoldas = 'meg'
const shortev = ev.slice(-2)
const feladat = "fl";
const utmutato = "ut";
const forras = "for";
const megoldas = "meg";
const shortev = ev.slice(-2);
let flPdfUrl, utPdfUrl, flZipUrl, utZipUrl, flMp3Url
switch (vizsgatargy) {
case 'inf':
case 'infoism':
case 'digkult':
flZipUrl = `${baseUrl}${prefix}${forras}_${shortev}${honap}_${feladat}.zip`
flPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`
utZipUrl = `${baseUrl}${prefix}${megoldas}_${shortev}${honap}_${utmutato}.zip`
utPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`
break
case 'angol':
case 'nemet':
flPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`
utPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`
flMp3Url = `${baseUrl}${prefix}_${shortev}${honap}_${feladat}.mp3`
break
default:
flPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`
utPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`
break
}
let flPdfUrl: string | undefined,
utPdfUrl: string | undefined,
flZipUrl: string | undefined,
utZipUrl: string | undefined,
flMp3Url: string | undefined;
switch (vizsgatargy) {
case "inf":
case "infoism":
case "digkult":
flZipUrl = `${baseUrl}${prefix}${forras}_${shortev}${honap}_${feladat}.zip`;
flPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`;
utZipUrl = `${baseUrl}${prefix}${megoldas}_${shortev}${honap}_${utmutato}.zip`;
utPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`;
break;
case "angol":
case "nemet":
flPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`;
utPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`;
flMp3Url = `${baseUrl}${prefix}_${shortev}${honap}_${feladat}.mp3`;
break;
default:
flPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`;
utPdfUrl = `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`;
break;
}
res.setHeader('Cache-Control', 's-maxage=31536000')
res.status(200).json({ flPdfUrl, utPdfUrl, flZipUrl, utZipUrl, flMp3Url })
} catch (error) {
res.status(500).json({ error: 'Internal Server Error', message: error })
}
res.setHeader("Cache-Control", "s-maxage=31536000");
res.status(200).json({ flPdfUrl, utPdfUrl, flZipUrl, utZipUrl, flMp3Url });
} catch (e: unknown) {
if (e instanceof Error) {
let causeCode: string | undefined;
if (e.cause && typeof e.cause === "object" && "code" in e.cause) {
causeCode = String((e.cause as { code: unknown }).code);
}
res.status(500).json({
error: "Internal Server Error",
message: e.message,
cause: causeCode,
stack: process.env.NODE_ENV === "development" ? e.stack : undefined,
});
} else {
res.status(500).json({
error: "Internal Server Error",
message: "An unexpected error occurred",
details: String(e),
});
}
}
}
+68 -36
View File
@@ -1,45 +1,77 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import type { NextApiRequest, NextApiResponse } from "next";
import { Agent, fetch } from "undici";
const insecureAgent = new Agent({
connect: {
rejectUnauthorized: false,
},
});
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { link } = req.query as { link: string }
let missingParam: string | null = null
if (!link) {
missingParam = 'link'
}
const { link } = req.query as { link: string };
let missingParam: string | null = null;
if (!link) {
missingParam = "link";
}
if (missingParam) {
return res.status(400).json({ error: `Hiányzó paraméter: ${missingParam}` })
}
if (missingParam) {
return res
.status(400)
.json({ error: `Hiányzó paraméter: ${missingParam}` });
}
const domain = link.split('/')[2]
if (domain !== 'dload-oktatas.educatio.hu') {
return res.status(400).json({ error: 'Érvénytelen link' })
}
const domain = link.split("/")[2];
if (domain !== "dload-oktatas.educatio.hu") {
return res.status(400).json({ error: "Érvénytelen link" });
}
try {
res.setHeader('Cache-Control', 's-maxage=31536000')
const response = await fetch(link, { method: 'GET' })
const contentType = response.headers.get('content-type')
try {
res.setHeader("Cache-Control", "s-maxage=31536000");
if (contentType == 'application/pdf') {
const filename = link.split('/').pop() ?? 'document.pdf'
res.setHeader('Content-Type', contentType)
res.setHeader('Content-Disposition', `inline; filename="${filename}"`)
}
const response = await fetch(link, {
method: "GET",
dispatcher: insecureAgent,
});
if (response.ok) {
const arrayBuffer: ArrayBuffer = await response.arrayBuffer()
const buffer: Buffer = Buffer.from(arrayBuffer)
res.send(buffer)
} else {
res
.status(response.status)
.json({ error: 'Hiba történt a lekérés során.' })
}
} catch (error) {
res.status(500).json({ error: 'Internal Server Error', message: error })
}
const contentType = response.headers.get("content-type");
if (contentType === "application/pdf") {
const filename = link.split("/").pop() ?? "document.pdf";
res.setHeader("Content-Type", contentType);
res.setHeader("Content-Disposition", `inline; filename="${filename}"`);
}
if (response.ok) {
const arrayBuffer: ArrayBuffer = await response.arrayBuffer();
const buffer: Buffer = Buffer.from(arrayBuffer);
res.send(buffer);
} else {
res
.status(response.status)
.json({ error: "Hiba történt a lekérés során." });
}
} catch (e: unknown) {
if (e instanceof Error) {
let causeCode: string | undefined;
if (e.cause && typeof e.cause === "object" && "code" in e.cause) {
causeCode = String((e.cause as { code: unknown }).code);
}
res.status(500).json({
error: "Internal Server Error",
message: e.message,
cause: causeCode,
stack: process.env.NODE_ENV === "development" ? e.stack : undefined,
});
} else {
res.status(500).json({
error: "Internal Server Error",
message: "An unexpected error occurred",
details: String(e),
});
}
}
}
+50 -30
View File
@@ -1,40 +1,60 @@
import type { NextApiRequest, NextApiResponse } from 'next'
import type { NextApiRequest, NextApiResponse } from "next";
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
req: NextApiRequest,
res: NextApiResponse,
) {
const { link } = req.query as { link: string }
let missingParam: string | null = null
const { link } = req.query as { link: string };
let missingParam: string | null = null;
if (!link) {
missingParam = 'link'
}
if (!link) {
missingParam = "link";
}
if (missingParam) {
return res.status(400).json({ error: `Hiányzó paraméter: ${missingParam}` })
}
if (missingParam) {
return res
.status(400)
.json({ error: `Hiányzó paraméter: ${missingParam}` });
}
const domain = link.split('/')[2]
if (
domain !== 'localhost:3000' &&
domain !== 'erettsegi.albert.lol' &&
domain !== 'dload-oktatas.educatio.hu'
) {
return res.status(400).json({ error: 'Érvénytelen link' })
}
const domain = link.split("/")[2];
if (
domain !== "localhost:3000" &&
domain !== "erettsegi.albert.lol" &&
domain !== "dload-oktatas.educatio.hu"
) {
return res.status(400).json({ error: "Érvénytelen link" });
}
try {
const { protocol, host } = new URL(link)
if (!protocol || !host) {
return res.status(400).json({ error: 'Érvénytelen link' })
}
try {
const { protocol, host } = new URL(link);
if (!protocol || !host) {
return res.status(400).json({ error: "Érvénytelen link" });
}
const response = await fetch(link, { method: 'HEAD' })
const response = await fetch(link, { method: "HEAD" });
const status = response.status
res.status(200).json({ status })
} catch (error) {
res.status(500).json({ error: 'Internal Server Error', message: error })
}
const status = response.status;
res.status(200).json({ status });
} catch (e: unknown) {
if (e instanceof Error) {
let causeCode: string | undefined;
if (e.cause && typeof e.cause === "object" && "code" in e.cause) {
causeCode = String((e.cause as { code: unknown }).code);
}
res.status(500).json({
error: "Internal Server Error",
message: e.message,
cause: causeCode,
stack: process.env.NODE_ENV === "development" ? e.stack : undefined,
});
} else {
res.status(500).json({
error: "Internal Server Error",
message: "An unexpected error occurred",
details: String(e),
});
}
}
}
+122 -112
View File
@@ -1,119 +1,129 @@
import React, { useEffect } from 'react'
import { ButtonGroup, Divider } from '@nextui-org/react'
import { PdfButton, ZipButton, Mp3Button } from '@/components/Buttons'
import { Footer } from '@/components/Footer'
import { fetchData } from '@/utils/fetch'
import useYears from '@/hooks/useYears'
import { useAppState } from '@/hooks/useState'
import { ButtonGroup, Divider } from "@heroui/react";
import { useEffect } from "react";
import { Mp3Button, PdfButton, ZipButton } from "@/components/Buttons";
import { Footer } from "@/components/Footer";
import {
SubjectSelector,
YearSelector,
PeriodSelector,
LevelSelector,
} from '@/components/Selectors'
import { subjects } from '@/utils/subjects'
LevelSelector,
PeriodSelector,
SubjectSelector,
YearSelector,
} from "@/components/Selectors";
import { useAppState } from "@/hooks/useState";
import useYears from "@/hooks/useYears";
import { fetchData } from "@/utils/fetch";
import { subjects } from "@/utils/subjects";
export default function Home() {
const {
flPdfLink,
setflPdfLink,
utPdfLink,
setutPdfLink,
flZipLink,
setflZipLink,
utZipLink,
setutZipLink,
flMp3Link,
setflMp3Link,
selectedSubject,
setSelectedSubject,
selectedYear,
setSelectedYear,
selectedPeriod,
setSelectedPeriod,
selectedLevel,
setSelectedLevel,
years,
setYears,
} = useAppState()
const {
flPdfLink,
setflPdfLink,
utPdfLink,
setutPdfLink,
flZipLink,
setflZipLink,
utZipLink,
setutZipLink,
flMp3Link,
setflMp3Link,
selectedSubject,
setSelectedSubject,
selectedYear,
setSelectedYear,
selectedPeriod,
setSelectedPeriod,
selectedLevel,
setSelectedLevel,
years,
setYears,
} = useAppState();
useYears(setYears)
useYears(setYears);
useEffect(() => {
if (selectedLevel && selectedPeriod && selectedSubject && selectedYear) {
void fetchData(
selectedSubject,
selectedYear,
selectedPeriod,
selectedLevel,
setflZipLink,
setutZipLink,
setflPdfLink,
setutPdfLink,
setflMp3Link,
)
}
}, [selectedLevel, selectedPeriod, selectedSubject, selectedYear])
useEffect(() => {
if (selectedLevel && selectedPeriod && selectedSubject && selectedYear) {
void fetchData(
selectedSubject,
selectedYear,
selectedPeriod,
selectedLevel,
setflZipLink,
setutZipLink,
setflPdfLink,
setutPdfLink,
setflMp3Link,
);
}
}, [
selectedLevel,
selectedPeriod,
selectedSubject,
selectedYear,
setutPdfLink,
setflZipLink,
setutZipLink,
setflPdfLink,
setflMp3Link,
]);
return (
<main className='dark:bg-[#121212] text-foreground bg-background py-5'>
<h1 className='text-4xl font-bold text-blue-400 text-center mt-16'>
Érettségi kereső
</h1>
<div className='flex min-h-screen flex-col items-center justify-between'>
<div className='container mx-auto'>
<div className='flex flex-col items-center justify-center'>
<div className='mt-5 mb-3'>
<SubjectSelector
selectedSubject={selectedSubject}
setSelectedSubject={setSelectedSubject}
subjects={subjects}
/>
</div>
<div className='mb-3'>
<YearSelector
selectedYear={selectedYear}
setSelectedYear={setSelectedYear}
years={years}
/>
</div>
<div className='mb-3'>
<PeriodSelector
selectedPeriod={selectedPeriod}
setSelectedPeriod={setSelectedPeriod}
/>
</div>
<div className='mb-3'>
<LevelSelector
selectedLevel={selectedLevel}
setSelectedLevel={setSelectedLevel}
/>
</div>
<div className='space-x-3'>
<ButtonGroup>
<PdfButton label='Feladatlap' link={flPdfLink} />
<Divider orientation='vertical' />
<PdfButton label='Útmutató' link={utPdfLink} />
</ButtonGroup>
</div>
{['inf', 'infoism', 'digkult'].includes(selectedSubject) && (
<div className='space-x-3'>
<ButtonGroup>
<ZipButton label='Forrás' link={flZipLink} />
<Divider orientation='vertical' />
<ZipButton label='Megoldás' link={utZipLink} />
</ButtonGroup>
</div>
)}
{['angol', 'nemet'].includes(selectedSubject) && (
<div className='space-x-3'>
<Mp3Button label='Hang' link={flMp3Link} />
</div>
)}
</div>
</div>
</div>
<Footer />
</main>
)
return (
<main className="dark:bg-[#121212] text-foreground bg-background py-5">
<h1 className="text-4xl font-bold text-blue-400 text-center mt-16">
Érettségi kereső
</h1>
<div className="flex min-h-screen flex-col items-center justify-between">
<div className="container mx-auto">
<div className="flex flex-col items-center justify-center">
<div className="mt-5 mb-3">
<SubjectSelector
selectedSubject={selectedSubject}
setSelectedSubject={setSelectedSubject}
subjects={subjects}
/>
</div>
<div className="mb-3">
<YearSelector
selectedYear={selectedYear}
setSelectedYear={setSelectedYear}
years={years}
/>
</div>
<div className="mb-3">
<PeriodSelector
selectedPeriod={selectedPeriod}
setSelectedPeriod={setSelectedPeriod}
/>
</div>
<div className="mb-3">
<LevelSelector
selectedLevel={selectedLevel}
setSelectedLevel={setSelectedLevel}
/>
</div>
<div className="space-x-3">
<ButtonGroup>
<PdfButton label="Feladatlap" link={flPdfLink} />
<Divider orientation="vertical" />
<PdfButton label="Útmutató" link={utPdfLink} />
</ButtonGroup>
</div>
{["inf", "infoism", "digkult"].includes(selectedSubject) && (
<div className="space-x-3">
<ButtonGroup>
<ZipButton label="Forrás" link={flZipLink} />
<Divider orientation="vertical" />
<ZipButton label="Megoldás" link={utZipLink} />
</ButtonGroup>
</div>
)}
{["angol", "nemet"].includes(selectedSubject) && (
<div className="space-x-3">
<Mp3Button label="Hang" link={flMp3Link} />
</div>
)}
</div>
</div>
</div>
<Footer />
</main>
);
}
+19 -7
View File
@@ -1,10 +1,22 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@plugin './hero.ts';
@source '../../node_modules/@heroui/theme/dist/**/*.{js,ts,jsx,tsx}';
@custom-variant dark (&:is(.dark *));
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
body {
background-color: #121212;
margin: 0;
height: 100%;
overflow: hidden;
background-color: #121212;
margin: 0;
height: 100%;
overflow: hidden;
}
+2
View File
@@ -0,0 +1,2 @@
import { heroui } from "@heroui/react";
export default heroui();
+38 -38
View File
@@ -1,45 +1,45 @@
export const fetchData = async (
selectedSubject: string,
selectedYear: string,
selectedPeriod: string,
selectedLevel: string,
setflZipLink: (link: string) => void,
setutZipLink: (link: string) => void,
setflPdfLink: (link: string) => void,
setutPdfLink: (link: string) => void,
setflMp3Link: (link: string) => void
selectedSubject: string,
selectedYear: string,
selectedPeriod: string,
selectedLevel: string,
setflZipLink: (link: string) => void,
setutZipLink: (link: string) => void,
setflPdfLink: (link: string) => void,
setutPdfLink: (link: string) => void,
setflMp3Link: (link: string) => void,
) => {
try {
const url = `/api/erettsegi?vizsgatargy=${selectedSubject}&ev=${selectedYear}&idoszak=${selectedPeriod}&szint=${selectedLevel}`
try {
const url = `/api/erettsegi?vizsgatargy=${selectedSubject}&ev=${selectedYear}&idoszak=${selectedPeriod}&szint=${selectedLevel}`;
const response = await fetch(url)
const response = await fetch(url);
if (response.ok) {
const data = (await response.json()) as {
flZipUrl: string
utZipUrl: string
flPdfUrl: string
utPdfUrl: string
flMp3Url: string
}
if (response.ok) {
const data = (await response.json()) as {
flZipUrl: string;
utZipUrl: string;
flPdfUrl: string;
utPdfUrl: string;
flMp3Url: string;
};
if (data.utZipUrl && data.flZipUrl) {
setflZipLink(data.flZipUrl)
setutZipLink(data.utZipUrl)
}
if (data.utZipUrl && data.flZipUrl) {
setflZipLink(data.flZipUrl);
setutZipLink(data.utZipUrl);
}
if (data.utPdfUrl && data.flPdfUrl) {
setflPdfLink(data.flPdfUrl)
setutPdfLink(data.utPdfUrl)
}
if (data.utPdfUrl && data.flPdfUrl) {
setflPdfLink(data.flPdfUrl);
setutPdfLink(data.utPdfUrl);
}
if (data.flMp3Url) {
setflMp3Link(data.flMp3Url)
}
} else {
console.error('Hiba történt az API hívás során.')
}
} catch (error) {
console.error('Hiba történt az API hívás során.', error)
}
}
if (data.flMp3Url) {
setflMp3Link(data.flMp3Url);
}
} else {
console.error("Hiba történt az API hívás során.");
}
} catch (error) {
console.error("Hiba történt az API hívás során.", error);
}
};
+12 -12
View File
@@ -1,17 +1,17 @@
export interface SelectorProps {
years: string[]
subjects: { label: string; value: string }[]
selectedSubject: string
selectedYear: string
selectedPeriod: string
selectedLevel: string
setSelectedSubject: React.Dispatch<React.SetStateAction<string>>
setSelectedYear: React.Dispatch<React.SetStateAction<string>>
setSelectedPeriod: React.Dispatch<React.SetStateAction<string>>
setSelectedLevel: React.Dispatch<React.SetStateAction<string>>
years: string[];
subjects: { label: string; value: string }[];
selectedSubject: string;
selectedYear: string;
selectedPeriod: string;
selectedLevel: string;
setSelectedSubject: React.Dispatch<React.SetStateAction<string>>;
setSelectedYear: React.Dispatch<React.SetStateAction<string>>;
setSelectedPeriod: React.Dispatch<React.SetStateAction<string>>;
setSelectedLevel: React.Dispatch<React.SetStateAction<string>>;
}
export interface ButtonProps {
label: string
link: string
label: string;
link: string;
}
+15 -15
View File
@@ -1,16 +1,16 @@
export const subjects = [
{ value: 'magyir', label: 'Magyar nyelv és irodalom' },
{ value: 'mat', label: 'Matematika' },
{ value: 'tort', label: 'Történelem' },
{ value: 'angol', label: 'Angol nyelv' },
{ value: 'nemet', label: 'Német nyelv' },
{ value: 'inf', label: 'Informatika' },
{ value: 'digkult', label: 'Digitális kultúra' },
{ value: 'bio', label: 'Biológia' },
{ value: 'infoism', label: 'Informatikai ismeretek' },
{ value: 'ker', label: 'Kereskedelmi ismeretek' },
{ value: 'kozg', label: 'Közgazdasági ismeretek' },
{ value: 'kem', label: 'Kémia' },
{ value: 'fldr', label: 'Földrajz' },
{ value: 'fiz', label: 'Fizika' },
]
{ value: "magyir", label: "Magyar nyelv és irodalom" },
{ value: "mat", label: "Matematika" },
{ value: "tort", label: "Történelem" },
{ value: "angol", label: "Angol nyelv" },
{ value: "nemet", label: "Német nyelv" },
{ value: "inf", label: "Informatika" },
{ value: "digkult", label: "Digitális kultúra" },
{ value: "bio", label: "Biológia" },
{ value: "infoism", label: "Informatikai ismeretek" },
{ value: "ker", label: "Kereskedelmi ismeretek" },
{ value: "kozg", label: "Közgazdasági ismeretek" },
{ value: "kem", label: "Kémia" },
{ value: "fldr", label: "Földrajz" },
{ value: "fiz", label: "Fizika" },
];
+7 -7
View File
@@ -1,8 +1,8 @@
export type ButtonColor =
| 'primary'
| 'danger'
| 'default'
| 'secondary'
| 'success'
| 'warning'
| undefined
| "primary"
| "danger"
| "default"
| "secondary"
| "success"
| "warning"
| undefined;