diff --git a/.dockerignore b/.dockerignore index 60425a0..a4e834b 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,3 +11,8 @@ public/videos/* # Local environment files may contain deployment secrets. .env* + +# Never package applicant documents or local test credentials +private +.local-verification +.career-tests-* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6728c8..9190365 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,9 @@ jobs: - name: Run TypeScript checks run: yarn tsc --noEmit + - name: Test career applications + run: yarn test:career + container: name: Build Docker image needs: quality diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 20f8503..a3e4216 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -50,6 +50,9 @@ jobs: - name: Run TypeScript checks run: yarn tsc --noEmit + - name: Test career applications + run: yarn test:career + build: name: Build and publish image needs: quality diff --git a/.gitignore b/.gitignore index 1955d09..262dec2 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,7 @@ yarn-error.log* next-env.d.ts /.idea/ /yarn.lock -public/videos \ No newline at end of file +public/videos +# Private applicant documents and local verification artifacts +/private/ +/.local-verification/ diff --git a/README.md b/README.md index 53e3c57..d3ad91a 100644 --- a/README.md +++ b/README.md @@ -150,3 +150,36 @@ See [docs/deployment.md](docs/deployment.md) for environment setup, required sec - Theming and reusable UI components If you have questions or want to contribute, feel free to open an issue or PR. + +### Szerkeszthető űrlap és önéletrajzok + +A **Karrier oldal → Űrlap kérdései** fülön szerkeszthetők a magyar/angol feliratok, +segédszövegek, sorrend, kötelezőség és láthatóság. Új kérdésként rövid vagy hosszú +szöveg, egy- vagy többválasztós mező és jelölőnégyzet adható hozzá. A név, e-mail +és hozzájárulás kötelező; a csoport és pozíció mező megőrzi az automatikus kitöltést. +A hozzájárulás mindig az űrlap végén jelenik meg. A kérdés- és opcióazonosítók +állandók. A jelentkezések a beküldéskori kérdéseket, opciókat és válaszokat is tárolják. +Ha kitöltés közben módosul az űrlap, a beküldés frissíti a kérdéseket és újraellenőrzést +kér, miközben a beírt válaszok és a kiválasztott CV megmaradnak. + +Az opcionális CV egy érvényes, nem titkosított PDF lehet, maximum **5 MiB**. +A fájl a privát `application-cvs` kollekcióba kerül, csak bejelentkezett admin +olvashatja vagy töltheti le. A jelentkezés törlése a hozzá tartozó fájlt is törli. +A nyilvános médiatárba nem kerül önéletrajz. Helyi tárolási útvonal: +`private/cvs`; a `CV_STORAGE_DIR` változóval felülírható. Dockerben külön, +környezetenként elkülönített `applicant_cvs` kötet tárolja a fájlokat. + +Állítsd be a **`NEXT_PUBLIC_SERVER_URL`** változót az adott környezet nyilvános +HTTPS címére (helyben például `http://localhost:3107`). A Google-táblázat a +korábbi tíz oszlop után a **További válaszok** és **Jelentkezés az adminban** +oszlopokat kapja. A második linkről adminbelépés után tölthető le a CV. +Ha a K–L oszlopban már saját adatok vannak, az export hibát jelez, és nem írja +felül őket: előbb helyezd át ezeket, majd használd az újraszinkronizálás gombot. +A jelentkezés és CV táblázathiba esetén is megmarad. + +A **Kapcsolat** bejegyzésben szerkeszthető a kiállítási gomb HTTPS linkje és +magyar/angol felirata. Üres linknél a gomb nem jelenik meg. Ezeket a beállításokat +csak bejelentkezett admin módosíthatja. + +Ellenőrzések: `yarn test:career`, `yarn tsc --noEmit`, `yarn eslint <érintett fájlok>`. +Adatbázis-frissítés: `yarn payload:migrate`; generált típusok: `yarn payload:generate-types`. diff --git a/docker-compose.yml b/docker-compose.yml index 62e5748..1f00a6b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -25,6 +25,7 @@ services: dockerfile: Dockerfile volumes: - frt:/app/media + - applicant_cvs:/app/private/cvs - ./public/videos:/app/public/videos depends_on: postgres: @@ -47,6 +48,7 @@ services: volumes: postgres: frt: + applicant_cvs: networks: postgres: diff --git a/docs/deployment.md b/docs/deployment.md index 6aadfae..3a0bd14 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -182,3 +182,34 @@ cd /srv/frt-preview ``` Ez sem végez helyi Docker buildet. + +## Privát önéletrajzok mentése és helyreállítása + +A Compose `applicant_cvs` kötete az `/app/private/cvs` könyvtárra csatlakozik. +A staging és production külön Compose projektnevét továbbra is kötelező használni. +A helyi CV-ket és tesztadatokat a Docker build és a Git figyelmen kívül hagyja. +A `NEXT_PUBLIC_SERVER_URL` az adott környezet publikus címe legyen, hogy a +Google-táblázatban szereplő adminhivatkozások teljes URL-ek legyenek. + +A production telepítési script az adatbázismentés mellé `postgres--cvs.tar.gz` +fájlt is készít. Sikertelen fájlmentés esetén a migráció nem indul el. Mindkét +mentés személyes adatokat tartalmazhat; a script 600-as fájljogosultságot állít be. +Az adatbázis és a CV-archívum együtt kezelendő. Konzisztens kézi mentéshez és +helyreállításhoz állítsd le az alkalmazás írásait/alkalmazáskonténerét. + +Helyreállítás próbája kizárólag elkülönített környezetben: + +1. Az adatbázis dumpját állítsd vissza egy külön PostgreSQL-adatbázisba. +2. Az azonos időpontú archívumot bontsd ki az elkülönített privát kötetbe + (`tar -xzf -C `; az archívumban `cvs/` szerepel). +3. Indítsd el az alkalmazást ezzel az adatbázissal és `CV_STORAGE_DIR` útvonallal. +4. Adminbelépéssel tölts le egy önéletrajzot, és hasonlítsd össze az eredeti + SHA-256 lenyomatával. Belépés nélkül ugyanez a URL legyen elutasítva. +5. Újraindítás után ismételd meg a letöltést. Az eredeti környezetet a próba nem módosíthatja. + +A korábbi migrációs snapshotok már tartalmazták a Kapcsolat-táblát, de az +üres adatbázist felépítő migrációs lánc nem. A `contact_baseline` migráció ezt +`IF NOT EXISTS` műveletekkel pótolja, meglévő kapcsolati adatok megőrzésével. +Visszavonása szándékosan nem törli a táblát. A következő migráció az új kérdéseket, +CV-kapcsolatot és kiállítási gombmezőket adja hozzá; éles adatokat tartalmazó +adatbázison visszavonás helyett előre javító migrációt használj. diff --git a/eslint.config.mjs b/eslint.config.mjs index f9c8d48..f8275c8 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,6 +14,9 @@ const eslintConfig = [ ignores: [ ".next/**", "coverage/**", + ".local-verification/**", + ".career-tests-*/**", + "private/**", "next-env.d.ts", "node_modules/**", "src/payload-types.ts", diff --git a/karrier-ellenorzes.md b/karrier-ellenorzes.md new file mode 100644 index 0000000..6f2a9fe --- /dev/null +++ b/karrier-ellenorzes.md @@ -0,0 +1,32 @@ +# Karrierfejlesztés – helyi ellenőrzés + +2026. szeptember 6. Helyi alkalmazás: http://127.0.0.1:3107/karrier + +- `yarn build`: sikeres (187 másodperc). Két meglévő, az érintett funkcióktól független lintfigyelmeztetés maradt a `car-section.tsx` és `image-gallery.tsx` fájlokban. +- `yarn tsc --noEmit`: sikeres. Célzott ESLint, `git diff --check`, a telepítési script szintaxisellenőrzése és a staging Compose konfiguráció ellenőrzése sikeres. +- `yarn test:career`: 12/12 teszt sikeres. Kötelező alapmezők, választós mezők, hosszkorlát, hibás PDF és feltöltési méret, JSON/multipart bemenet, fejlécütközés, szinkronhiba és teljes 12 oszlopos RAW export. +- Valódi helyi HTTP-próbák: régi JSON beküldés; dinamikus kötelező kérdés és hamis opció; konfigurációváltozás (409); PDF mentés/letöltés; névtelen fájlhozzáférés (403); hiányzó adminjog és hibás kiállítási URL; zárt/külső jelentkezés; honeypot és kéréskorlát (429). +- Kényszerített adatbázishiba után a feltöltött CV takarítása sikeres. Jelentkezés törlésekor a CV rekordja és fájlja is eltűnik. A konfigurációból törölt kérdés válaszai és eredeti feliratai a korábbi jelentkezésben megmaradnak. +- Látható Chromium-böngésző: 1440 px asztali, 390 px mobil és 420 px magas képernyő; magyar/angol cím és menü; kilenc csoport; GYIK a jelentkezés előtt; pozíció-előtöltés; HU beküldés CV-vel és EN beküldés CV nélkül. A panel és az első csoport felső éle azonos, a GYIK-ugrás a felső menü alá érkezik. Nem volt JavaScript-oldalhiba. +- Adminból új, kötelező, kétnyelvű kérdés létrehozása és mentése sikeres. Egy korábban megnyitott űrlap beküldése jelezte a változást, megőrizte a válaszokat és a CV-t; az új kérdés kitöltése után sikeres volt a beküldés. A jelentkezés az adminban CV-vel és olvasható válaszösszesítővel jelenik meg. +- A buildből indított standalone alkalmazáson az üres GYIK, a zárt/külső jelentkezési mód, a billentyűzetes szakasznavigáció és az újraindítás utáni privát CV-letöltés is sikeresen ellenőrizve. +- A kiállítási gomb új lapon megnyitotta a megadott Google-űrlapot, címe: „BME FRT - Rendezvényen való kiállítási igény”. +- Adatbázis- és CV-mentés visszaállítása külön adatbázisba és könyvtárba sikeres. A CV-hivatkozás és a fájl SHA-256 lenyomata egyezik. A visszaállított adatbázison az új migráció visszavonása és ismételt futtatása sikeres; a régi jelentkezés és a kapcsolati tartalom megmaradt. + +A helyi adatbázis `frt_career`, külön Docker-konténerben, az 55439-es porton fut. +A kilenc csoport, a kapcsolati szöveg és a jelentkezések helyi tesztadatok. +A helyi admin adatai a Gitből és a Docker buildből kizárt +`.local-verification/admin.json` fájlban vannak. Az eredeti `.env` változatlan; +a helyi környezet külön `.env.local` beállításokat használ, üres Google-szolgáltatásfiókkal. + +A Google Sheets-exportot szimulált API-válaszokkal ellenőriztük, valódi táblázatba +nem írtunk. Élesítéskor a dokumentált publikus URL-beállítás és a tartós privát +CV-kötet szükséges. Az ellenőrzés helyi környezetben történt; push és élesítés nem történt. + +## Világos téma kontrasztjavítása + +- A karrieroldal külön szemantikus színkészletet használ. Világos módban semleges kártyák, sötét szövegek, jól látható mezőkeretek és piros, fehér feliratú gombok jelennek meg. A korábbi globális színfelülírások nem érintik ezeket az elemeket. +- Látható Chromium: világos magyar 1440/390 px, világos angol 1440 px, sötét magyar 1440 px és sötét angol 390 px. Összesen 38 állapotellenőrzés: alapnézet, nyitott csoport/pozíció, oldalsáv, GYIK, hover, mezőfókusz, hibás és sikeres beküldés. Nem volt vízszintes túlcsordulás vagy JavaScript-oldalhiba. +- A helyi teszttartalom megjelenített szövegeinek legkisebb mért kontrasztja 5,34:1. A normál mezőkeretek kontrasztja a környező kártyán világos módban 4,02:1, sötét módban 4,56:1. A sikerpipa kontrasztja világos módban 6,40:1. Ez a vizsgált tartalomra és állapotokra vonatkozó mérés, nem teljes webhely-audit. +- A beküldési állapotokat szimulált HTTP-válaszokkal ellenőriztük; ez az ellenőrzés nem hozott létre jelentkezéseket. Részletes mérések és képek: `.local-verification/career-contrast-results.json`, `career-light-form.png`, `career-light-success.png` és a témánkénti képernyőképek. +- `yarn tsc --noEmit`, a karrieroldal célzott ESLint-ellenőrzése és `git diff --check`: sikeres. A helyi fejlesztői szerver továbbra is a 3107-es porton fut. diff --git a/karrier-visszajelzesek-terv.md b/karrier-visszajelzesek-terv.md new file mode 100644 index 0000000..5fa813d --- /dev/null +++ b/karrier-visszajelzesek-terv.md @@ -0,0 +1,18 @@ +# Karrieroldal és kiállítási igény – javítási terv + +Megvalósítva 2026. szeptember 6-án. Az alkalmazás és az admin ellenőrzése elkülönített helyi PostgreSQL-adatbázissal és tesztadatokkal történt. A build, a típusellenőrzés és a 12 célzott teszt sikeres; éles környezetet nem módosítottunk. + +Javasolt alapbeállítás: adminból szerkeszthető kérdések; opcionális, egyetlen PDF önéletrajz, legfeljebb 5 MiB; angol főcím: „Join our team!”. Ezek a jóváhagyott terv szerinti alapbeállítások. + +- [x] **1. Oldalszerkezet és főcím.** A `career-page-client.tsx` címét és bevezetőjét a teljes tartalmi konténert átfogó sorba emelni. Alatta közös kéthasábos elrendezés: balra csoportok → GYIK → jelentkezés, jobbra az első csoport felső éléhez igazított panel. A főcím „Csatlakozz csapatunkhoz!”, a magyar menüfelirat „Karrier”. A fordításokat a `translations.ts` kezeli. Mobilon egyhasábos tartalom marad. Elfogadás: hosszabb bevezetővel is az első csoport mellett kezdődik a panel. +- [x] **2. Oldalsáv és szakasznavigáció.** A `career-sidebar.tsx` három eleme: alapból csukott „Csoportok” lenyíló, „Gyakran ismételt kérdések” ugrólink, jelentkezési gomb. A lista a CMS aktuális csoportjait használja, nincs beégetett kilences darabszám. Csoportválasztáskor a célcsoport kinyílik és odagörgetünk; a listát visszacsukjuk. A GYIK stabil `gyik` horgonyt kap, linkje csak az adott nyelven megjelenő GYIK esetén látszik. A `use-career-page.ts` görgetése és a sticky távolság a felső menü alá igazodik; alacsony képernyőn minden panelvezérlő elérhető. Billentyűzet, fókuszjelzés, `aria-expanded`, csökkentett mozgás támogatása. A zárt jelentkezés és a külső Google-űrlap mód viselkedése megmarad. +- [x] **3. Menüsorrend.** A `src/config/navigation.ts` listában a Karrier a Rólunk után kerül. Elfogadás: asztali és mobilmenüben, magyarul és angolul is ez az utolsó tartalmi menüpont; a láthatósági beállítás tovább működik. +- [x] **4. Szerkeszthető jelentkezési kérdések.** A `CareerSettings.ts` új „Űrlap kérdései” fülén a meglévő mezők felirata, súgója és sorrendje szerkeszthető; az opcionális mezők elrejthetők vagy kötelezővé tehetők. Név, e-mail és hozzájárulás kötelező alapmező marad, a csoport/pozíció automatikus kitöltése megmarad. Új kérdésként rövid/hosszú szöveg, egy- vagy többválasztós kérdés és jelölőnégyzet adható hozzá, HU/EN felirattal és opciókkal. Stabil kérdés- és opcióazonosítók; az `application-form.tsx` és a szervervalidáció ugyanazt a konfigurációt követi. A `JobApplications.ts` az új válaszok mellé a beküldéskori kérdésszöveget, opciókat, nyelvet és konfigurációverziót is menti. Kérdés törlése a régi válaszokat nem törli; beküldés közbeni konfigurációváltozásnál érthető hibajelzés, adatvesztés nélkül. Elfogadás: adminban hozzáadott kérdés megjelenik, validálódik és visszaolvasható. +- [x] **5. CV-feltöltés.** A kérdések után, a hozzájárulás és beküldés előtt fájlmező: fájlnév, eltávolítás/csere, típus- és mérethatár jelzése. A `/api/karrier/jelentkezes` multipart beküldést fogad, szerveroldalon is ellenőrzi a méretet és PDF-tartalmat; a korábbi JSON beküldés támogatása megmarad. Új privát upload-kollekció és kapcsolat a jelentkezéshez: beküldés kizárólag az ellenőrzött API-n át, olvasás/letöltés csak belépett adminnak. Külön tartós Docker-kötet, például `/app/private/cvs`, környezetenként elkülönítve, mentéssel és helyreállítási próbával. Hibás jelentkezés után ne maradjon árva fájl; jelentkezéstörlés a CV-t is takarítsa. Meglévő botvédelem megtartása, teljes kérésméret korlátozása. Elfogadás: CV-vel és nélküle is sikeres beküldés, hibás/túlméretes fájl elutasítása, névtelen letöltés tiltása, újraindítás után elérhető dokumentum. A jogosultsági megoldást a [Payload feltöltési dokumentációja](https://payloadcms.com/docs/upload/overview) támogatja. +- [x] **6. Google Sheets és adatmigráció.** A `karrier-sheet-sync.ts` / `google-sheets.ts` a meglévő tíz oszlop után „További válaszok” és „Jelentkezés az adminban” oszlopot kap; utóbbiból érhető el a CV is belépés után. Az összesítő a beküldéskori kérdés–válasz párokat tartalmazza. A jelenlegi fejléc csak üres táblánál jön létre, ezért ellenőrzött fejlécbővítés kell, idegen oszlop felülírása nélkül. Sheets-hiba nem akadályozhatja a jelentkezés/CV mentését; adminból újraszinkronizálható marad. Payload-migrációk, típusgenerálás és README-frissítés; a régi rekordok új mezői opcionálisak, a jelenlegi űrlap az induló konfiguráció. +- [x] **7. Kiállítási igény gomb.** A `Contact.ts` kollekcióban szerkeszthető URL és HU/EN gombfelirat, a `kapcsolat/page.tsx` innen olvas. A megadott [Google-űrlap](https://docs.google.com/forms/d/e/1FAIpQLSdrA3VX3YpxQB3uZ1ojXov8-J3LPawAt4ip_Aj4k8FNbNOCrA/viewform?usp=dialog) legyen az induló cél, a meglévő rekordot is kitöltő migrációval; üres URL-nél nincs gomb. HTTPS URL-validáció, új lapos megnyitás. A jelenlegi `Contact` nyilvános létrehozási/frissítési hozzáférését bejelentkezett adminra kell szűkíteni, hogy a publikus API-n se lehessen átírni a linket. Elfogadás: adminból módosított link jelenik meg, névtelen írás tiltott. A böngészős próba megnyitotta a „BME FRT - Rendezvényen való kiállítási igény” űrlapot. +- [x] **8. Végső ellenőrzés.** Elkülönített tesztadatbázison migráció és visszaolvasás régi jelentkezésekkel; TypeScript (`yarn tsc --noEmit`), célzott ESLint (`yarn eslint …`), build. Célzott API-tesztek a dinamikus validációra, fájlhozzáférésre és hibák utáni takarításra; Sheets-próba kizárólag teszttáblán. Böngészős próba HU/EN nyelven, mobilon és asztali/ alacsony képernyőn: sorrend, paneligazítás, csoport/ GYIK ugrás, pozíció-előtöltés, üres GYIK, zárt/külső jelentkezés, teljes beküldés és adminos CV-letöltés. Élesítés külön egyeztetett lépés. + +Megvalósítási sorrend: 1–3. megjelenés → 4–6. szerkeszthető űrlap és CV teljes adatútja → 7. kapcsolati gomb → 8. ellenőrzés. A megjelenési rész önállóan is szállítható; az űrlap, tárolás, export és migráció együtt alkot teljes változtatást. + +Ellenőrzési részletek: [karrier-ellenorzes.md](./karrier-ellenorzes.md). A Google Sheets-kéréseket izolált, szimulált teszttáblával ellenőriztük; valódi Google-táblázatba nem írtunk. diff --git a/package.json b/package.json index 2ced8c0..ab6245c 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "lint": "next lint", "payload:migrate": "node --import ./css-loader-register.mjs ./node_modules/.bin/payload migrate", "payload:migrate-status": "node --import ./css-loader-register.mjs ./node_modules/.bin/payload migrate:status", - "payload:generate-types": "node --import ./css-loader-register.mjs ./node_modules/.bin/payload generate:types" + "payload:generate-types": "node --import ./css-loader-register.mjs ./node_modules/.bin/payload generate:types", + "test:career": "node scripts/test-career.mjs" }, "dependencies": { "@fullcalendar/core": "^6.1.19", @@ -32,6 +33,7 @@ "lucide-react": "^0.511.0", "next": "15.5.9", "payload": "^3.80.0", + "pdf-lib": "^1.17.1", "react": "19.2.1", "react-dom": "19.2.1", "tailwind-merge": "^3.3.0", diff --git a/scripts/deploy-image.sh b/scripts/deploy-image.sh index f673fc5..3f5128c 100755 --- a/scripts/deploy-image.sh +++ b/scripts/deploy-image.sh @@ -144,6 +144,16 @@ if [[ "$backup_database" == "true" ]]; then rm -f "$backup_tmp" exit 1 fi + cv_backup_path="${backup_path%.dump}-cvs.tar.gz" + echo "Creating private CV backup at $cv_backup_path" + if compose_with "$candidate_env" run --rm -T --no-deps --entrypoint tar app \ + -C /app/private -czf - cvs > "$cv_backup_path.tmp"; then + mv "$cv_backup_path.tmp" "$cv_backup_path" + chmod 600 "$backup_path" "$cv_backup_path" + else + rm -f "$cv_backup_path.tmp" + exit 1 + fi fi echo "Running Payload migrations with the candidate image" diff --git a/scripts/test-career.mjs b/scripts/test-career.mjs new file mode 100644 index 0000000..17dc384 --- /dev/null +++ b/scripts/test-career.mjs @@ -0,0 +1,22 @@ +import { build } from "esbuild"; +import { mkdtemp, rm } from "node:fs/promises"; +import { spawnSync } from "node:child_process"; +import path from "node:path"; +const directory = await mkdtemp(path.join(process.cwd(), ".career-tests-")); +try { + const outfile = path.join(directory, "career.test.cjs"); + await build({ + entryPoints: ["tests/career.test.ts"], + outfile, + bundle: true, + platform: "node", + format: "cjs", + packages: "external", + }); + const result = spawnSync(process.execPath, ["--test", outfile], { + stdio: "inherit", + }); + process.exitCode = result.status ?? 1; +} finally { + await rm(directory, { recursive: true, force: true }); +} diff --git a/src/app/(app)/globals.css b/src/app/(app)/globals.css index 235fea8..2c8490a 100644 --- a/src/app/(app)/globals.css +++ b/src/app/(app)/globals.css @@ -1,5 +1,6 @@ @import "tailwindcss"; @import "tw-animate-css"; +@import "./karrier/career-theme.css"; @tailwind utilities; @plugin "@tailwindcss/typography"; diff --git a/src/app/(app)/kapcsolat/page.tsx b/src/app/(app)/kapcsolat/page.tsx index 66622bc..30a5728 100644 --- a/src/app/(app)/kapcsolat/page.tsx +++ b/src/app/(app)/kapcsolat/page.tsx @@ -50,12 +50,12 @@ export default async function ContactPage (props: { searchParams?: Promise - - {lang === 'en' ? 'Event Exhibition Request' : 'Rendezvényen való kiállítási igény'} - + {lang === 'en' ? (contact.exhibitionLabelEng || 'Event Exhibition Request') : (contact.exhibitionLabel || 'Rendezvényen való kiállítási igény')} + }
diff --git a/src/app/(app)/karrier/career-page-client.tsx b/src/app/(app)/karrier/career-page-client.tsx index 14b3e0e..a82c9ec 100644 --- a/src/app/(app)/karrier/career-page-client.tsx +++ b/src/app/(app)/karrier/career-page-client.tsx @@ -2,6 +2,7 @@ import type { CareerSetting, Recruitment } from "@/payload-types" import { RichText } from "@payloadcms/richtext-lexical/react" +import { resolveFormConfig } from "@/lib/career-form" import { resolveApplicationConfig } from "./career-config" import ApplicationForm from "./components/application-form" import ApplyButton from "./components/apply-button" @@ -17,12 +18,26 @@ interface CareerPageClientProps { lang: string } -export default function CareerPageClient({ recruitmentData, careerSettings, lang }: CareerPageClientProps) { +export default function CareerPageClient({ + recruitmentData, + careerSettings, + lang, +}: CareerPageClientProps) { const isEn = isEnglish(lang) const texts = careerTexts(isEn) - const application = resolveApplicationConfig(careerSettings, isEn, texts.applicationsClosed) + const application = resolveApplicationConfig( + careerSettings, + isEn, + texts.applicationsClosed, + ) const intro = isEn ? careerSettings?.introEng : careerSettings?.intro const faqs = careerSettings?.faqs ?? [] + const hasFaqs = faqs.some((item) => + Boolean( + (isEn ? item.questionEng : item.question)?.trim() && + (isEn ? item.answerEng : item.answer), + ), + ) const { formRef, @@ -37,70 +52,94 @@ export default function CareerPageClient({ recruitmentData, careerSettings, lang } = useCareerPage() return ( -
-
-
-

{texts.title}

+
+
+
+

+ {texts.title} +

{intro ? (
) : ( -

{texts.subtitle}

+

+ {texts.subtitle} +

)} +
+
+
+
+ {recruitmentData.map((group) => ( + toggleGroup(group.id)} + onTogglePosition={togglePosition} + onApply={applyForPosition} + /> + ))} +
-
- {recruitmentData.map((group) => ( - toggleGroup(group.id)} - onTogglePosition={togglePosition} - onApply={applyForPosition} - /> - ))} -
+ -
- {!application.isOpen ? ( -
-

{application.closedText}

-
- ) : application.useBuiltInForm ? ( - - ) : ( - application.googleFormUrl && ( -
-

{texts.interested}

- +
+ {!application.isOpen ? ( +
+

+ {application.closedText} +

- ) - )} + ) : application.useBuiltInForm ? ( + + ) : ( + application.googleFormUrl && ( +
+

+ {texts.interested} +

+ +
+ ) + )} +
- +
- -
-
+
) } diff --git a/src/app/(app)/karrier/career-theme.css b/src/app/(app)/karrier/career-theme.css new file mode 100644 index 0000000..1bbb856 --- /dev/null +++ b/src/app/(app)/karrier/career-theme.css @@ -0,0 +1,71 @@ +/* Career colors are independent of the legacy global utility overrides. */ +@theme inline { + --color-career-page: var(--career-page); + --color-career-surface: var(--career-surface); + --color-career-inset: var(--career-inset); + --color-career-hover: var(--career-hover); + --color-career-text: var(--career-text); + --color-career-muted: var(--career-muted); + --color-career-accent: var(--career-accent); + --color-career-accent-soft: var(--career-accent-soft); + --color-career-border: var(--career-border); + --color-career-input-border: var(--career-input-border); + --color-career-field: var(--career-field); + --color-career-action: var(--career-action); + --color-career-action-hover: var(--career-action-hover); + --color-career-on-action: var(--career-on-action); +} + +.career-page { + --career-page: #000000; + --career-surface: #230505; + --career-inset: #140404; + --career-hover: #3b1015; + --career-text: #fafafa; + --career-muted: #c7babb; + --career-accent: #ff8290; + --career-accent-soft: #421820; + --career-border: #663b40; + --career-input-border: #927478; + --career-field: #080808; + --career-action: #d51028; + --career-action-hover: #b20e21; + --career-on-action: #ffffff; + background-color: var(--career-page); + color: var(--career-text); +} + +html.light .career-page { + --career-page: #ffffff; + --career-surface: #f7f7f8; + --career-inset: #ffffff; + --career-hover: #f0e7e9; + --career-text: #211c22; + --career-muted: #59515a; + --career-accent: #b51025; + --career-accent-soft: #f9e4e7; + --career-border: #dad6d8; + --career-input-border: #82777b; + --career-field: #ffffff; +} + +/* CMS paragraphs and headings follow their surrounding text hierarchy. */ +.career-page .rich-text-content :is(p, h1, h2, h3, h4, h5, h6, li) { + color: inherit; +} + +.career-page .rich-text-content a { + color: var(--career-accent); + text-decoration: underline; + text-underline-offset: 3px; +} + +.career-page :is(button, a, input, textarea, select, [tabindex]):focus-visible { + outline: 2px solid var(--career-accent); + outline-offset: 3px; +} + +/* Inset outlines stay visible inside clipped accordion cards. */ +.career-page button[aria-expanded]:focus-visible { + outline-offset: -3px; +} diff --git a/src/app/(app)/karrier/components/application-form.tsx b/src/app/(app)/karrier/components/application-form.tsx index 8f2ad3e..5f1df3f 100644 --- a/src/app/(app)/karrier/components/application-form.tsx +++ b/src/app/(app)/karrier/components/application-form.tsx @@ -1,193 +1,402 @@ "use client" -import { AlertCircle, CheckCircle2, Loader2 } from "lucide-react" -import { useEffect, useState } from "react" +import { AlertCircle, Loader2, Paperclip, X } from "lucide-react" +import { useEffect, useRef, useState } from "react" +import { + MAX_CV_SIZE, + validateAnswers, + type Answer, + type FormConfig, + type FormQuestion, +} from "@/lib/career-form" import { applicationErrorText, applicationFormTexts } from "../translations" import type { SelectedPosition } from "../types" -import { CheckboxField, HoneypotField, TextAreaField, TextField } from "./form-field" +import { HoneypotField } from "./form-field" +import SuccessCheck from "./success-check" interface ApplicationFormProps { lang: string isEn: boolean selectedPosition: SelectedPosition | null + initialConfig: FormConfig } +const INPUT_CLASS = + "w-full rounded-lg border border-career-input-border bg-career-field px-4 py-2.5 text-career-text placeholder:text-career-muted focus:border-career-accent focus:outline-none focus:ring-1 focus:ring-career-accent" -type Status = "idle" | "submitting" | "success" | "error" - -const EMPTY_FORM = { - name: "", - email: "", - phone: "", - university: "", - major: "", - semester: "", - groupName: "", - positionName: "", - motivation: "", - consent: false, - website: "", +function Question({ + question: q, + value, + onChange, + isEn, + error, +}: { + question: FormQuestion + value?: Answer + onChange: (value: Answer) => void + isEn: boolean + error: boolean +}) { + const id = `application-${q.key}` + const label = isEn ? q.labelEng : q.label + const hint = isEn ? q.hintEng : q.hint + const labelContent = ( + <> + {label} + {q.required ? ( + " *" + ) : ( + + {" "} + ({isEn ? "optional" : "opcionális"}) + + )} + + ) + const common = { + id, + name: q.key, + "aria-invalid": error || undefined, + "aria-describedby": hint ? `${id}-hint` : undefined, + } + const onMultiChange = (key: string, checked: boolean) => { + const selected = Array.isArray(value) ? value : [] + onChange( + checked ? [...selected, key] : selected.filter((v) => v !== key), + ) + } + return ( +
+ {q.type === "multiselect" ? ( +
+ + {labelContent} + +
+ {q.options?.map((option) => ( + + ))} +
+
+ ) : q.type === "checkbox" ? ( + + ) : ( + <> + + {q.type === "textarea" ? ( +