mirror of
https://github.com/skidoodle/erettsegi-browser.git
synced 2026-04-28 13:37:35 +02:00
app router
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { subjects } from "@/utils/subjects";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const vizsgatargy = searchParams.get("vizsgatargy");
|
||||
const ev = searchParams.get("ev");
|
||||
const idoszak = searchParams.get("idoszak");
|
||||
const szint = searchParams.get("szint");
|
||||
|
||||
if (!vizsgatargy || !ev || !idoszak || !szint) {
|
||||
const missingParams = [];
|
||||
if (!vizsgatargy) missingParams.push("vizsgatargy");
|
||||
if (!ev) missingParams.push("ev");
|
||||
if (!idoszak) missingParams.push("idoszak");
|
||||
if (!szint) missingParams.push("szint");
|
||||
|
||||
return NextResponse.json(
|
||||
{ error: `Hiányzó paraméterek: ${missingParams.join(", ")}` },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (parseInt(ev, 10) <= 2012) {
|
||||
return NextResponse.json({ error: "Érvénytelen év" }, { status: 400 });
|
||||
}
|
||||
|
||||
const validSubjects = subjects.map((subject) => subject.value);
|
||||
if (!validSubjects.includes(vizsgatargy)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Érvénytelen vizsgatárgy" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
let honap: string;
|
||||
switch (idoszak) {
|
||||
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 =
|
||||
req.headers.get("x-forwarded-proto") === "https" ? "https" : "http";
|
||||
const host = req.headers.get("host");
|
||||
const baseUrl = `https://dload-oktatas.educatio.hu/erettsegi/feladatok_${ev}${idoszak}_${szint}/`;
|
||||
const proxiedUrl = `${protocol}://${host}/api/proxy?link=${encodeURI(baseUrl)}`;
|
||||
|
||||
const shortev = ev.slice(-2);
|
||||
const feladat = "fl";
|
||||
const utmutato = "ut";
|
||||
const forras = "for";
|
||||
const megoldas = "meg";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{ flPdfUrl, utPdfUrl, flZipUrl, utZipUrl, flMp3Url },
|
||||
{
|
||||
status: 200,
|
||||
headers: { "Cache-Control": "s-maxage=31536000" },
|
||||
},
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
console.error("An unexpected error occurred in the API route:", e);
|
||||
return NextResponse.json(
|
||||
{ error: "Internal Server Error" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { Agent, fetch } from "undici";
|
||||
|
||||
const insecureAgent = new Agent({
|
||||
connect: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const link = searchParams.get("link");
|
||||
|
||||
if (!link) {
|
||||
return NextResponse.json(
|
||||
{ error: "Hiányzó paraméter: link" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(link);
|
||||
} catch (_error) {
|
||||
return NextResponse.json(
|
||||
{ error: "Érvénytelen link formátum" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (url.hostname !== "dload-oktatas.educatio.hu") {
|
||||
return NextResponse.json({ error: "Érvénytelen link" }, { status: 400 });
|
||||
}
|
||||
|
||||
const externalResponse = await fetch(link, {
|
||||
method: "GET",
|
||||
dispatcher: insecureAgent,
|
||||
});
|
||||
|
||||
if (!externalResponse.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: "Hiba történt a külső forrás lekérése során." },
|
||||
{ status: externalResponse.status },
|
||||
);
|
||||
}
|
||||
|
||||
const body = await externalResponse.arrayBuffer();
|
||||
|
||||
const contentType = externalResponse.headers.get("content-type");
|
||||
const headers = new Headers();
|
||||
headers.set("Cache-Control", "s-maxage=31536000, stale-while-revalidate");
|
||||
|
||||
if (contentType) {
|
||||
headers.set("Content-Type", contentType);
|
||||
if (contentType === "application/pdf") {
|
||||
const filename = url.pathname.split("/").pop() ?? "document.pdf";
|
||||
headers.set("Content-Disposition", `inline; filename="${filename}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return new Response(body, {
|
||||
status: 200,
|
||||
headers: headers,
|
||||
});
|
||||
} catch (e: unknown) {
|
||||
console.error("Proxy Error:", e);
|
||||
if (e instanceof Error) {
|
||||
return NextResponse.json(
|
||||
{ error: "Internal Server Error", message: e.message },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: "An unexpected internal error occurred" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { Agent, fetch } from "undici";
|
||||
|
||||
const insecureAgent = new Agent({
|
||||
connect: {
|
||||
rejectUnauthorized: false,
|
||||
},
|
||||
});
|
||||
|
||||
const ALLOWED_HOSTS = [
|
||||
"localhost:3000",
|
||||
"erettsegi.albert.lol",
|
||||
"dload-oktatas.educatio.hu",
|
||||
];
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
try {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const link = searchParams.get("link");
|
||||
|
||||
if (!link) {
|
||||
return NextResponse.json(
|
||||
{ error: "Hiányzó paraméter: link" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(link);
|
||||
} catch (_error) {
|
||||
return NextResponse.json(
|
||||
{ error: "Érvénytelen link formátum" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
if (!ALLOWED_HOSTS.includes(url.host)) {
|
||||
return NextResponse.json({ error: "Érvénytelen link" }, { status: 400 });
|
||||
}
|
||||
|
||||
const response = await fetch(link, {
|
||||
method: "HEAD",
|
||||
dispatcher: insecureAgent,
|
||||
});
|
||||
|
||||
return NextResponse.json({ status: response.status }, { status: 200 });
|
||||
} catch (e: unknown) {
|
||||
console.error("Validation Error:", e);
|
||||
|
||||
if (e instanceof Error) {
|
||||
const cause = e.cause as { code?: unknown } | undefined;
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Internal Server Error",
|
||||
message: e.message,
|
||||
cause: cause?.code ? String(cause.code) : undefined,
|
||||
stack: process.env.NODE_ENV === "development" ? e.stack : undefined,
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Internal Server Error",
|
||||
message: "An unexpected error occurred",
|
||||
details: String(e),
|
||||
},
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Metadata, Viewport } from "next";
|
||||
import Script from "next/script";
|
||||
import { Providers } from "@/app/providers";
|
||||
import { Inter } from "next/font/google";
|
||||
import "@/styles/globals.css";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-inter",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
metadataBase: new URL("https://erettsegi.albert.lol"),
|
||||
title: "Érettségi kereső",
|
||||
description: "Egyszerű keresés és letöltés az érettségi feladatsorokhoz. 🏫",
|
||||
icons: {
|
||||
icon: "/favicon.ico",
|
||||
},
|
||||
openGraph: {
|
||||
title: "Érettségi kereső",
|
||||
description:
|
||||
"Egyszerű keresés és letöltés az érettségi feladatsorokhoz. 🏫",
|
||||
url: "https://erettsegi.albert.lol",
|
||||
images: "/logo.png",
|
||||
},
|
||||
};
|
||||
|
||||
export const viewport: Viewport = {
|
||||
themeColor: "#121212",
|
||||
width: "device-width",
|
||||
initialScale: 1,
|
||||
maximumScale: 1,
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="hu" suppressHydrationWarning>
|
||||
<Script
|
||||
defer
|
||||
src="https://analytics.albert.lol/script.js"
|
||||
data-website-id="7b196f47-39c9-4b8e-8dfd-b6e707282eea"
|
||||
/>
|
||||
<body className={`${inter.variable} antialiased font-sans`}>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@heroui/button";
|
||||
import { Footer } from "@/components/Footer";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function NotFound() {
|
||||
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">
|
||||
<Link href="/">
|
||||
<Button color="primary">Vissza</Button>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Footer />
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
|
||||
import { ButtonGroup, Divider } from "@heroui/react";
|
||||
import { useEffect } from "react";
|
||||
import { Mp3Button, PdfButton, ZipButton } from "@/components/Buttons";
|
||||
import { Footer } from "@/components/Footer";
|
||||
import {
|
||||
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();
|
||||
|
||||
useYears(setYears);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { HeroUIProvider } from "@heroui/react";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<HeroUIProvider>
|
||||
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={true}>
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</HeroUIProvider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user