Minor updates

This commit is contained in:
2025-06-20 21:43:42 +02:00
parent e3326998c4
commit 4e1328147b
17 changed files with 344 additions and 369 deletions
+61 -68
View File
@@ -9,12 +9,46 @@ export async function GET(req: NextRequest) {
const idoszak = searchParams.get("idoszak"); const idoszak = searchParams.get("idoszak");
const szint = searchParams.get("szint"); const szint = searchParams.get("szint");
if (!vizsgatargy && !ev && !idoszak && !szint) {
const currentYear = new Date().getFullYear();
return NextResponse.json({
parameters: {
vizsgatargy: {
type: "string",
required: true,
values: subjects.map((s) => ({
value: s.value,
label: s.label,
})),
},
ev: {
type: "integer",
required: true,
values: `2013 - ${currentYear}`,
},
idoszak: {
type: "string",
required: true,
values: ["tavasz", "osz"],
},
szint: {
type: "string",
required: true,
values: ["kozep", "emelt"],
},
},
example:
"/api/erettsegi?vizsgatargy=mat&ev=2023&idoszak=tavasz&szint=kozep",
});
}
if (!vizsgatargy || !ev || !idoszak || !szint) { if (!vizsgatargy || !ev || !idoszak || !szint) {
const missingParams = []; const missingParams = [
if (!vizsgatargy) missingParams.push("vizsgatargy"); !vizsgatargy && "vizsgatargy",
if (!ev) missingParams.push("ev"); !ev && "ev",
if (!idoszak) missingParams.push("idoszak"); !idoszak && "idoszak",
if (!szint) missingParams.push("szint"); !szint && "szint",
].filter(Boolean);
return NextResponse.json( return NextResponse.json(
{ error: `Hiányzó paraméterek: ${missingParams.join(", ")}` }, { error: `Hiányzó paraméterek: ${missingParams.join(", ")}` },
@@ -34,41 +68,16 @@ export async function GET(req: NextRequest) {
); );
} }
let honap: string; const honap = idoszak === "osz" ? "okt" : "maj";
switch (idoszak) { const prefix = szint === "emelt" ? `e_${vizsgatargy}` : `k_${vizsgatargy}`;
case "osz":
honap = "okt";
break;
case "tavasz":
honap = "maj";
break;
default:
return NextResponse.json(
{ error: "Érvénytelen időszak" },
{ status: 400 },
);
}
let prefix: string;
switch (szint) {
case "emelt":
prefix = `e_${vizsgatargy}`;
break;
case "kozep":
prefix = `k_${vizsgatargy}`;
break;
default:
return NextResponse.json(
{ error: "Érvénytelen szint" },
{ status: 400 },
);
}
const protocol = const protocol =
req.headers.get("x-forwarded-proto") === "https" ? "https" : "http"; req.headers.get("x-forwarded-proto") === "https" ? "https" : "http";
const host = req.headers.get("host"); const host = req.headers.get("host");
const baseUrl = `https://dload-oktatas.educatio.hu/erettsegi/feladatok_${ev}${idoszak}_${szint}/`; const baseUrl = `https://dload-oktatas.educatio.hu/erettsegi/feladatok_${ev}${idoszak}_${szint}/`;
const proxiedUrl = `${protocol}://${host}/api/proxy?link=${encodeURI(baseUrl)}`; const proxiedUrl = `${protocol}://${host}/api/proxy?link=${encodeURI(
baseUrl,
)}`;
const shortev = ev.slice(-2); const shortev = ev.slice(-2);
const feladat = "fl"; const feladat = "fl";
@@ -76,40 +85,24 @@ export async function GET(req: NextRequest) {
const forras = "for"; const forras = "for";
const megoldas = "meg"; const megoldas = "meg";
let flPdfUrl: string | undefined, const urls = {
utPdfUrl: string | undefined, flPdfUrl: `${proxiedUrl}${prefix}_${shortev}${honap}_${feladat}.pdf`,
flZipUrl: string | undefined, utPdfUrl: `${proxiedUrl}${prefix}_${shortev}${honap}_${utmutato}.pdf`,
utZipUrl: string | undefined, flZipUrl: ["inf", "infoism", "digkult"].includes(vizsgatargy)
flMp3Url: string | undefined; ? `${baseUrl}${prefix}${forras}_${shortev}${honap}_${feladat}.zip`
: undefined,
utZipUrl: ["inf", "infoism", "digkult"].includes(vizsgatargy)
? `${baseUrl}${prefix}${megoldas}_${shortev}${honap}_${utmutato}.zip`
: undefined,
flMp3Url: ["angol", "nemet"].includes(vizsgatargy)
? `${baseUrl}${prefix}_${shortev}${honap}_${feladat}.mp3`
: undefined,
};
switch (vizsgatargy) { return NextResponse.json(urls, {
case "inf": status: 200,
case "infoism": headers: { "Cache-Control": "s-maxage=31536000" },
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;
}
return NextResponse.json(
{ flPdfUrl, utPdfUrl, flZipUrl, utZipUrl, flMp3Url },
{
status: 200,
headers: { "Cache-Control": "s-maxage=31536000" },
},
);
} catch (e: unknown) { } catch (e: unknown) {
console.error("An unexpected error occurred in the API route:", e); console.error("An unexpected error occurred in the API route:", e);
return NextResponse.json( return NextResponse.json(
+16 -14
View File
@@ -13,10 +13,15 @@ export async function GET(req: NextRequest) {
const link = searchParams.get("link"); const link = searchParams.get("link");
if (!link) { if (!link) {
return NextResponse.json( return NextResponse.json({
{ error: "Hiányzó paraméter: link" }, parameters: {
{ status: 400 }, link: {
); type: "string",
required: true,
},
},
example: `/api/proxy?link=https://dload-oktatas.educatio.hu/erettsegi/feladatok_2023tavasz_kozep/k_mat_23maj_fl.pdf`,
});
} }
let url: URL; let url: URL;
@@ -48,8 +53,9 @@ export async function GET(req: NextRequest) {
const body = await externalResponse.arrayBuffer(); const body = await externalResponse.arrayBuffer();
const contentType = externalResponse.headers.get("content-type"); const contentType = externalResponse.headers.get("content-type");
const headers = new Headers(); const headers = new Headers({
headers.set("Cache-Control", "s-maxage=31536000, stale-while-revalidate"); "Cache-Control": "s-maxage=31536000, stale-while-revalidate",
});
if (contentType) { if (contentType) {
headers.set("Content-Type", contentType); headers.set("Content-Type", contentType);
@@ -61,18 +67,14 @@ export async function GET(req: NextRequest) {
return new Response(body, { return new Response(body, {
status: 200, status: 200,
headers: headers, headers,
}); });
} catch (e: unknown) { } catch (e: unknown) {
console.error("Proxy Error:", e); console.error("Proxy Error:", e);
if (e instanceof Error) { const errorMessage =
return NextResponse.json( e instanceof Error ? e.message : "An unexpected internal error occurred";
{ error: "Internal Server Error", message: e.message },
{ status: 500 },
);
}
return NextResponse.json( return NextResponse.json(
{ error: "An unexpected internal error occurred" }, { error: "Internal Server Error", message: errorMessage },
{ status: 500 }, { status: 500 },
); );
} }
+17 -24
View File
@@ -19,10 +19,16 @@ export async function GET(req: NextRequest) {
const link = searchParams.get("link"); const link = searchParams.get("link");
if (!link) { if (!link) {
return NextResponse.json( return NextResponse.json({
{ error: "Hiányzó paraméter: link" }, parameters: {
{ status: 400 }, link: {
); type: "string",
required: true,
allowed_hosts: ALLOWED_HOSTS,
},
},
example: `/api/validate?link=https://dload-oktatas.educatio.hu/erettsegi/feladatok_2023tavasz_kozep/k_mat_23maj_fl.pdf`,
});
} }
let url: URL; let url: URL;
@@ -48,26 +54,13 @@ export async function GET(req: NextRequest) {
} catch (e: unknown) { } catch (e: unknown) {
console.error("Validation Error:", e); console.error("Validation Error:", e);
if (e instanceof Error) { const errorResponse = {
const cause = e.cause as { code?: unknown } | undefined; error: "Internal Server Error",
return NextResponse.json( message: e instanceof Error ? e.message : "An unexpected error occurred",
{ ...(process.env.NODE_ENV === "development" &&
error: "Internal Server Error", e instanceof Error && { stack: e.stack }),
message: e.message, };
cause: cause?.code ? String(cause.code) : undefined,
stack: process.env.NODE_ENV === "development" ? e.stack : undefined,
},
{ status: 500 },
);
}
return NextResponse.json( return NextResponse.json(errorResponse, { status: 500 });
{
error: "Internal Server Error",
message: "An unexpected error occurred",
details: String(e),
},
{ status: 500 },
);
} }
} }
+4 -1
View File
@@ -3,6 +3,7 @@ import Script from "next/script";
import { Providers } from "@/app/providers"; import { Providers } from "@/app/providers";
import { Inter } from "next/font/google"; import { Inter } from "next/font/google";
import "@/styles/globals.css"; import "@/styles/globals.css";
import { AppProvider } from "@/ctx/app";
const inter = Inter({ const inter = Inter({
subsets: ["latin"], subsets: ["latin"],
@@ -45,7 +46,9 @@ export default function RootLayout({
data-website-id="7b196f47-39c9-4b8e-8dfd-b6e707282eea" data-website-id="7b196f47-39c9-4b8e-8dfd-b6e707282eea"
/> />
<body className={`${inter.variable} antialiased font-sans`}> <body className={`${inter.variable} antialiased font-sans`}>
<Providers>{children}</Providers> <AppProvider>
<Providers>{children}</Providers>
</AppProvider>
</body> </body>
</html> </html>
); );
+9 -17
View File
@@ -8,23 +8,15 @@ export default function NotFound() {
return ( return (
<> <>
<main className="dark:bg-[#121212] text-foreground bg-background py-5"> <main className="dark:bg-[#121212] text-foreground bg-background py-5">
<h1 className="text-7xl font-bold text-blue-400 text-center mt-16"> <div className="flex min-h-screen flex-col items-center justify-center">
404 <h1 className="text-7xl font-bold text-blue-400">404</h1>
</h1> <div className="mt-5 mb-3 text-center">
<div className="flex min-h-screen flex-col items-center justify-between"> <p className="text-2xl font-semibold text-gray-600">
<div className="container mx-auto"> Az keresett oldal nem található.
<div className="flex flex-col items-center justify-center"> </p>
<div className="mt-5 mb-3"> <Link href="/" className="mt-8 inline-block">
<div className="text-2xl font-semibold text-gray-600"> <Button color="primary">Vissza a főoldalra</Button>
<p className="mt-2">Az keresett oldal nem található.</p> </Link>
<p className="mt-8 text-center">
<Link href="/">
<Button color="primary">Vissza</Button>
</Link>
</p>
</div>
</div>
</div>
</div> </div>
</div> </div>
<Footer /> <Footer />
+56 -57
View File
@@ -1,45 +1,38 @@
"use client"; "use client";
import { ButtonGroup, Divider } from "@heroui/react"; import { ButtonGroup, Divider } from "@heroui/react";
import { useEffect } from "react"; import { useContext, useEffect, useCallback } from "react";
import { Mp3Button, PdfButton, ZipButton } from "@/components/Buttons"; import { Resource } from "@/components/Resources";
import { Footer } from "@/components/Footer"; import { Footer } from "@/components/Footer";
import { import { Selector, periodItems, levelItems } from "@/components/Selectors";
LevelSelector, import { AppContext } from "@/ctx/app";
PeriodSelector,
SubjectSelector,
YearSelector,
} from "@/components/Selectors";
import { useAppState } from "@/hooks/useState";
import useYears from "@/hooks/useYears";
import { fetchData } from "@/utils/fetch"; import { fetchData } from "@/utils/fetch";
import { subjects } from "@/utils/subjects"; import { subjects } from "@/utils/subjects";
import useYears from "@/hooks/useYears";
export default function Home() { export default function Home() {
const { state, dispatch } = useContext(AppContext);
const { const {
flPdfLink, flPdfLink,
setflPdfLink,
utPdfLink, utPdfLink,
setutPdfLink,
flZipLink, flZipLink,
setflZipLink,
utZipLink, utZipLink,
setutZipLink,
flMp3Link, flMp3Link,
setflMp3Link,
selectedSubject, selectedSubject,
setSelectedSubject,
selectedYear, selectedYear,
setSelectedYear,
selectedPeriod, selectedPeriod,
setSelectedPeriod,
selectedLevel, selectedLevel,
setSelectedLevel,
years, years,
setYears, } = state;
} = useAppState();
useYears(setYears); const setYearsCallback = useCallback(
(newYears: string[]) => {
dispatch({ type: "SET_YEARS", payload: newYears });
},
[dispatch],
);
useYears(setYearsCallback);
useEffect(() => { useEffect(() => {
if (selectedLevel && selectedPeriod && selectedSubject && selectedYear) { if (selectedLevel && selectedPeriod && selectedSubject && selectedYear) {
@@ -48,24 +41,16 @@ export default function Home() {
selectedYear, selectedYear,
selectedPeriod, selectedPeriod,
selectedLevel, selectedLevel,
setflZipLink, dispatch,
setutZipLink,
setflPdfLink,
setutPdfLink,
setflMp3Link,
); );
} }
}, [ }, [selectedLevel, selectedPeriod, selectedSubject, selectedYear, dispatch]);
selectedLevel,
selectedPeriod, const subjectItems = subjects.map((subject) => ({
selectedSubject, value: subject.value,
selectedYear, label: subject.label,
setutPdfLink, }));
setflZipLink, const yearItems = years.map((year) => ({ value: year, label: year }));
setutZipLink,
setflPdfLink,
setflMp3Link,
]);
return ( return (
<main className="dark:bg-[#121212] text-foreground bg-background py-5"> <main className="dark:bg-[#121212] text-foreground bg-background py-5">
@@ -76,50 +61,64 @@ export default function Home() {
<div className="container mx-auto"> <div className="container mx-auto">
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<div className="mt-5 mb-3"> <div className="mt-5 mb-3">
<SubjectSelector <Selector
selectedSubject={selectedSubject} label="Tárgy"
setSelectedSubject={setSelectedSubject} selectedValue={selectedSubject}
subjects={subjects} onSelectionChange={(subject) =>
dispatch({ type: "SET_SELECTED_SUBJECT", payload: subject })
}
items={subjectItems}
/> />
</div> </div>
<div className="mb-3"> <div className="mb-3">
<YearSelector <Selector
selectedYear={selectedYear} label="Év"
setSelectedYear={setSelectedYear} selectedValue={selectedYear}
years={years} onSelectionChange={(year) =>
dispatch({ type: "SET_SELECTED_YEAR", payload: year })
}
items={yearItems}
/> />
</div> </div>
<div className="mb-3"> <div className="mb-3">
<PeriodSelector <Selector
selectedPeriod={selectedPeriod} label="Időszak"
setSelectedPeriod={setSelectedPeriod} selectedValue={selectedPeriod}
onSelectionChange={(period) =>
dispatch({ type: "SET_SELECTED_PERIOD", payload: period })
}
items={periodItems}
/> />
</div> </div>
<div className="mb-3"> <div className="mb-3">
<LevelSelector <Selector
selectedLevel={selectedLevel} label="Szint"
setSelectedLevel={setSelectedLevel} selectedValue={selectedLevel}
onSelectionChange={(level) =>
dispatch({ type: "SET_SELECTED_LEVEL", payload: level })
}
items={levelItems}
/> />
</div> </div>
<div className="space-x-3"> <div className="space-x-3">
<ButtonGroup> <ButtonGroup>
<PdfButton label="Feladatlap" link={flPdfLink} /> <Resource label="Feladatlap" link={flPdfLink} />
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<PdfButton label="Útmutató" link={utPdfLink} /> <Resource label="Útmutató" link={utPdfLink} />
</ButtonGroup> </ButtonGroup>
</div> </div>
{["inf", "infoism", "digkult"].includes(selectedSubject) && ( {["inf", "infoism", "digkult"].includes(selectedSubject) && (
<div className="space-x-3"> <div className="space-x-3">
<ButtonGroup> <ButtonGroup>
<ZipButton label="Forrás" link={flZipLink} /> <Resource label="Forrás" link={flZipLink} />
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<ZipButton label="Megoldás" link={utZipLink} /> <Resource label="Megoldás" link={utZipLink} />
</ButtonGroup> </ButtonGroup>
</div> </div>
)} )}
{["angol", "nemet"].includes(selectedSubject) && ( {["angol", "nemet"].includes(selectedSubject) && (
<div className="space-x-3"> <div className="space-x-3">
<Mp3Button label="Hang" link={flMp3Link} /> <Resource label="Hang" link={flMp3Link} />
</div> </div>
)} )}
</div> </div>
+2 -2
View File
@@ -3,11 +3,11 @@
import { Source } from "@/components/Source"; import { Source } from "@/components/Source";
import { ThemeSwitcher } from "@/components/ThemeSwitcher"; import { ThemeSwitcher } from "@/components/ThemeSwitcher";
export const Footer = () => { export function Footer() {
return ( return (
<div className="fixed bottom-0 py-5 left-0 right-0 text-center space-x-5"> <div className="fixed bottom-0 py-5 left-0 right-0 text-center space-x-5">
<Source /> <Source />
<ThemeSwitcher /> <ThemeSwitcher />
</div> </div>
); );
}; }
@@ -1,11 +1,16 @@
"use client"; "use client";
import { Button } from "@heroui/react"; import { Button, type ButtonProps as HeroButtonProps } from "@heroui/react";
import React, { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState, memo } from "react";
import type { ButtonProps } from "@/utils/props";
import type { ButtonColor } from "@/utils/types";
const CustomButton: React.FC<ButtonProps> = React.memo(({ label, link }) => { export interface ResourceProps {
label: string;
link: string;
}
type ButtonColor = HeroButtonProps["color"];
const ResourceComponent = ({ label, link }: ResourceProps) => {
const [status, setStatus] = useState<number>(); const [status, setStatus] = useState<number>();
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
@@ -21,6 +26,8 @@ const CustomButton: React.FC<ButtonProps> = React.memo(({ label, link }) => {
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
} else {
setStatus(undefined);
} }
}, [link]); }, [link]);
@@ -29,16 +36,10 @@ const CustomButton: React.FC<ButtonProps> = React.memo(({ label, link }) => {
}, [checkLinkStatus]); }, [checkLinkStatus]);
const getColor = useCallback((): ButtonColor => { const getColor = useCallback((): ButtonColor => {
switch (true) { if (isLoading) return "default";
case isLoading: if (status === 200) return "primary";
return "default"; if (status === 404) return "danger";
case status === 200: return "default";
return "primary";
case status === 404:
return "danger";
default:
return "default";
}
}, [isLoading, status]); }, [isLoading, status]);
const handleClick = useCallback(() => { const handleClick = useCallback(() => {
@@ -60,16 +61,6 @@ const CustomButton: React.FC<ButtonProps> = React.memo(({ label, link }) => {
{label} {label}
</Button> </Button>
); );
}); };
export const PdfButton: React.FC<ButtonProps> = React.memo( export const Resource = memo(ResourceComponent);
({ label, link }) => <CustomButton label={label} link={link} />,
);
export const ZipButton: React.FC<ButtonProps> = React.memo(
({ label, link }) => <CustomButton label={label} link={link} />,
);
export const Mp3Button: React.FC<ButtonProps> = React.memo(
({ label, link }) => <CustomButton label={label} link={link} />,
);
+47 -72
View File
@@ -1,79 +1,54 @@
"use client"; "use client";
import { Select, SelectItem } from "@heroui/react"; import { Select, SelectItem } from "@heroui/react";
import type { SelectorProps } from "@/utils/props";
import type { ChangeEvent } from "react";
export const SubjectSelector: React.FC< interface SelectorItem {
Pick<SelectorProps, "selectedSubject" | "setSelectedSubject" | "subjects"> value: string;
> = ({ selectedSubject, setSelectedSubject, subjects }) => ( label: string;
<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< interface SelectorProps {
Pick<SelectorProps, "selectedYear" | "setSelectedYear" | "years"> label: string;
> = ({ selectedYear, setSelectedYear, years }) => ( selectedValue: string;
<Select onSelectionChange: (value: string) => void;
selectionMode="single" items: SelectorItem[];
disallowEmptySelection={true} className?: string;
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< export const periodItems = [
Pick<SelectorProps, "selectedPeriod" | "setSelectedPeriod"> { value: "tavasz", label: "Tavasz" },
> = ({ selectedPeriod, setSelectedPeriod }) => ( { value: "osz", label: "Ősz" },
<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< export const levelItems = [
Pick<SelectorProps, "selectedLevel" | "setSelectedLevel"> { value: "kozep", label: "Közép" },
> = ({ selectedLevel, setSelectedLevel }) => ( { value: "emelt", label: "Emelt" },
<Select ];
selectionMode="single"
disallowEmptySelection={true} export function Selector({
label="Szint" label,
value={selectedLevel} selectedValue,
onChange={(e: ChangeEvent<HTMLSelectElement>) => onSelectionChange,
setSelectedLevel(e.target.value) items,
} className = "w-56",
className="w-56" }: SelectorProps) {
> return (
<SelectItem key={"kozep"}>Közép</SelectItem> <Select
<SelectItem key={"emelt"}>Emelt</SelectItem> selectionMode="single"
</Select> disallowEmptySelection={true}
); label={label}
selectedKeys={selectedValue ? [selectedValue] : []}
onSelectionChange={(keys) => {
const key = Array.from(keys)[0];
if (key) {
onSelectionChange(String(key));
}
}}
className={className}
>
{items.map((item) => (
<SelectItem key={item.value}>{item.label}</SelectItem>
))}
</Select>
);
}
+2 -2
View File
@@ -3,7 +3,7 @@
import { Button } from "@heroui/button"; import { Button } from "@heroui/button";
import { VscGithubInverted } from "react-icons/vsc"; import { VscGithubInverted } from "react-icons/vsc";
export const Source = () => { export function Source() {
return ( return (
<Button <Button
aria-label="Source Code" aria-label="Source Code"
@@ -15,4 +15,4 @@ export const Source = () => {
<VscGithubInverted size={20} /> <VscGithubInverted size={20} />
</Button> </Button>
); );
}; }
+2 -2
View File
@@ -4,7 +4,7 @@ import { Button } from "@heroui/button";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { VscColorMode } from "react-icons/vsc"; import { VscColorMode } from "react-icons/vsc";
export const ThemeSwitcher = () => { export function ThemeSwitcher() {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const toggle = () => { const toggle = () => {
@@ -20,4 +20,4 @@ export const ThemeSwitcher = () => {
)} )}
</Button> </Button>
); );
}; }
+91
View File
@@ -0,0 +1,91 @@
"use client";
import {
createContext,
useReducer,
type Dispatch,
type ReactNode,
} from "react";
interface State {
flPdfLink: string;
utPdfLink: string;
flZipLink: string;
utZipLink: string;
flMp3Link: string;
selectedSubject: string;
selectedYear: string;
selectedPeriod: string;
selectedLevel: string;
years: string[];
}
const initialState: State = {
flPdfLink: "",
utPdfLink: "",
flZipLink: "",
utZipLink: "",
flMp3Link: "",
selectedSubject: "",
selectedYear: "",
selectedPeriod: "",
selectedLevel: "",
years: [],
};
type Action =
| { type: "SET_FL_PDF_LINK"; payload: string }
| { type: "SET_UT_PDF_LINK"; payload: string }
| { type: "SET_FL_ZIP_LINK"; payload: string }
| { type: "SET_UT_ZIP_LINK"; payload: string }
| { type: "SET_FL_MP3_LINK"; payload: string }
| { type: "SET_SELECTED_SUBJECT"; payload: string }
| { type: "SET_SELECTED_YEAR"; payload: string }
| { type: "SET_SELECTED_PERIOD"; payload: string }
| { type: "SET_SELECTED_LEVEL"; payload: string }
| { type: "SET_YEARS"; payload: string[] };
const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "SET_FL_PDF_LINK":
return { ...state, flPdfLink: action.payload };
case "SET_UT_PDF_LINK":
return { ...state, utPdfLink: action.payload };
case "SET_FL_ZIP_LINK":
return { ...state, flZipLink: action.payload };
case "SET_UT_ZIP_LINK":
return { ...state, utZipLink: action.payload };
case "SET_FL_MP3_LINK":
return { ...state, flMp3Link: action.payload };
case "SET_SELECTED_SUBJECT":
return { ...state, selectedSubject: action.payload };
case "SET_SELECTED_YEAR":
return { ...state, selectedYear: action.payload };
case "SET_SELECTED_PERIOD":
return { ...state, selectedPeriod: action.payload };
case "SET_SELECTED_LEVEL":
return { ...state, selectedLevel: action.payload };
case "SET_YEARS":
return { ...state, years: action.payload };
default:
return state;
}
};
export const AppContext = createContext<{
state: State;
dispatch: Dispatch<Action>;
}>({
state: initialState,
dispatch: () => null,
});
export const AppProvider = ({ children }: { children: ReactNode }) => {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<AppContext.Provider value={{ state, dispatch }}>
{children}
</AppContext.Provider>
);
};
-42
View File
@@ -1,42 +0,0 @@
"use client";
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[]>([]);
useYears(setYears);
return {
flPdfLink,
setflPdfLink,
utPdfLink,
setutPdfLink,
flZipLink,
setflZipLink,
utZipLink,
setutZipLink,
flMp3Link,
setflMp3Link,
selectedSubject,
setSelectedSubject,
selectedYear,
setSelectedYear,
selectedPeriod,
setSelectedPeriod,
selectedLevel,
setSelectedLevel,
years,
setYears,
};
};
+1 -3
View File
@@ -2,9 +2,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
export default function useYears( export default function useYears(setYears: (years: string[]) => void) {
setYears: React.Dispatch<React.SetStateAction<string[]>>,
) {
useEffect(() => { useEffect(() => {
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const availableYears: string[] = []; const availableYears: string[] = [];
+18 -13
View File
@@ -1,13 +1,18 @@
import type { Dispatch } from "react";
type Action =
| { type: "SET_FL_ZIP_LINK"; payload: string }
| { type: "SET_UT_ZIP_LINK"; payload: string }
| { type: "SET_FL_PDF_LINK"; payload: string }
| { type: "SET_UT_PDF_LINK"; payload: string }
| { type: "SET_FL_MP3_LINK"; payload: string };
export const fetchData = async ( export const fetchData = async (
selectedSubject: string, selectedSubject: string,
selectedYear: string, selectedYear: string,
selectedPeriod: string, selectedPeriod: string,
selectedLevel: string, selectedLevel: string,
setflZipLink: (link: string) => void, dispatch: Dispatch<Action>,
setutZipLink: (link: string) => void,
setflPdfLink: (link: string) => void,
setutPdfLink: (link: string) => void,
setflMp3Link: (link: string) => void,
) => { ) => {
try { try {
const url = `/api/erettsegi?vizsgatargy=${selectedSubject}&ev=${selectedYear}&idoszak=${selectedPeriod}&szint=${selectedLevel}`; const url = `/api/erettsegi?vizsgatargy=${selectedSubject}&ev=${selectedYear}&idoszak=${selectedPeriod}&szint=${selectedLevel}`;
@@ -16,25 +21,25 @@ export const fetchData = async (
if (response.ok) { if (response.ok) {
const data = (await response.json()) as { const data = (await response.json()) as {
flZipUrl: string; flZipUrl?: string;
utZipUrl: string; utZipUrl?: string;
flPdfUrl: string; flPdfUrl: string;
utPdfUrl: string; utPdfUrl: string;
flMp3Url: string; flMp3Url?: string;
}; };
if (data.utZipUrl && data.flZipUrl) { if (data.utZipUrl && data.flZipUrl) {
setflZipLink(data.flZipUrl); dispatch({ type: "SET_FL_ZIP_LINK", payload: data.flZipUrl });
setutZipLink(data.utZipUrl); dispatch({ type: "SET_UT_ZIP_LINK", payload: data.utZipUrl });
} }
if (data.utPdfUrl && data.flPdfUrl) { if (data.utPdfUrl && data.flPdfUrl) {
setflPdfLink(data.flPdfUrl); dispatch({ type: "SET_FL_PDF_LINK", payload: data.flPdfUrl });
setutPdfLink(data.utPdfUrl); dispatch({ type: "SET_UT_PDF_LINK", payload: data.utPdfUrl });
} }
if (data.flMp3Url) { if (data.flMp3Url) {
setflMp3Link(data.flMp3Url); dispatch({ type: "SET_FL_MP3_LINK", payload: data.flMp3Url });
} }
} else { } else {
console.error("Hiba történt az API hívás során."); console.error("Hiba történt az API hívás során.");
-17
View File
@@ -1,17 +0,0 @@
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>>;
}
export interface ButtonProps {
label: string;
link: string;
}
-8
View File
@@ -1,8 +0,0 @@
export type ButtonColor =
| "primary"
| "danger"
| "default"
| "secondary"
| "success"
| "warning"
| undefined;