From 2b1068004a41bad268da499e457e844cd347b444 Mon Sep 17 00:00:00 2001 From: DankaMarci Date: Fri, 3 Jul 2026 14:26:35 +0200 Subject: [PATCH 1/2] feat: implement redesigned dormitories overview page with CMS integration --- .../kollegium/kollegium-bemutato/page.tsx | 109 +++++++++++++++- src/collections/Dormitories.ts | 73 +++++++++++ src/components/common/ImageCard.tsx | 120 ++++++++++++------ src/dictionaries/en/dormitories.json | 6 +- src/dictionaries/hu/dormitories.json | 6 +- src/lib/payload-cms.ts | 13 ++ .../20260703_140815_dormitories_collection.ts | 40 ++++++ src/migrations/index.ts | 6 + src/payload-types.ts | 41 ++++++ src/payload.config.ts | 2 + 10 files changed, 368 insertions(+), 48 deletions(-) create mode 100644 src/collections/Dormitories.ts create mode 100644 src/migrations/20260703_140815_dormitories_collection.ts diff --git a/src/app/(app)/[lang]/kollegium/kollegium-bemutato/page.tsx b/src/app/(app)/[lang]/kollegium/kollegium-bemutato/page.tsx index 7403ba3..2aec70d 100644 --- a/src/app/(app)/[lang]/kollegium/kollegium-bemutato/page.tsx +++ b/src/app/(app)/[lang]/kollegium/kollegium-bemutato/page.tsx @@ -1,18 +1,115 @@ +export const dynamic = "force-dynamic"; + import { PageHeader } from "@/components/common/PageHeader"; import { getDictionary } from "@/get-dictionary"; import { Locale } from "@/i18n-config"; -import DormitoryCardsContainer from "../felveteli-tajekoztato/components/DormitoryCardsContainer"; +import { getDormitories } from "@/lib/payload-cms"; +import { Media } from "@/payload-types"; +import { ImageCard } from "@/components/common/ImageCard"; + +const fallbackDormitories = [ + { + name: "Baross Gábor Kollégium", + slug: "baross", + imageSrc: "/kolik/baross.jpg", + }, + { + name: "Bercsényi 28-30 Kollégium", + slug: "bercsenyi", + imageSrc: "/kolik/bercsenyi.jpg", + }, + { + name: "Kármán Tódor Kollégium", + slug: "karman", + imageSrc: "/kolik/karman.jpg", + }, + { + name: "Martos Kollégium", + slug: "martos", + imageSrc: "/kolik/martos.jpg", + }, + { + name: "Schönherz Kollégium", + slug: "sch", + imageSrc: "/kolik/schonherz.jpg", + }, + { + name: "Vásárhelyi Pál Kollégium", + slug: "vpk", + imageSrc: "/kolik/vasarhelyi.jpg", + }, + { + name: "Wigner Jenő Kollégium", + slug: "wigner", + imageSrc: "/kolik/wigner.jpg", + }, +]; -export default async function AdmissionInformationPage({ +export default async function DormitoriesOverviewPage({ params }: Readonly<{ params: Promise<{ lang: Locale }> }>){ const { lang } = await params; - const dictionary = await getDictionary(lang, 'dormitories'); + const [dictionary, dormitories] = await Promise.all([ + getDictionary(lang, "dormitories"), + getDormitories(), + ]); + + const d = dictionary.dormitories.admission_information; + + const cards = dormitories + .map((dormitory) => { + const coverImage = + typeof dormitory.coverImage === "object" && dormitory.coverImage !== null + ? (dormitory.coverImage as Media) + : null; + + if (!coverImage?.url) { + return null; + } + + return { + name: dormitory.name, + slug: dormitory.slug, + imageSrc: coverImage.url, + href: + dormitory.externalLink || + `/${lang}/kollegium/kollegium-bemutato/${dormitory.slug}`, + }; + }) + .filter((card): card is { name: string; slug: string; imageSrc: string; href: string } => card !== null); + + const renderedCards = + cards.length > 0 + ? cards + : fallbackDormitories.map((dormitory) => ({ + ...dormitory, + href: `/${lang}/kollegium/kollegium-bemutato/${dormitory.slug}`, + })); return ( -
+
- - + +
+ {renderedCards.length > 0 ? ( +
+ {renderedCards.map((dormitory) => ( + + ))} +
+ ) : ( +

+ {d.no_results} +

+ )} +
) diff --git a/src/collections/Dormitories.ts b/src/collections/Dormitories.ts new file mode 100644 index 0000000..3145226 --- /dev/null +++ b/src/collections/Dormitories.ts @@ -0,0 +1,73 @@ +import type { CollectionConfig } from "payload"; + +const validateOptionalUrl = (val: string | null | undefined) => { + if (!val) return true; + + try { + const parsed = new URL(val); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return "A linknek érvényes HTTP vagy HTTPS URL-nek kell lennie (pl. https://example.com)."; + } + return true; + } catch { + return "Érvénytelen URL formátum."; + } +}; + +export const Dormitories: CollectionConfig = { + slug: "dormitories", + labels: { + singular: "Kollégium", + plural: "Kollégiumok", + }, + access: { + read: () => true, + }, + admin: { + useAsTitle: "name", + defaultColumns: ["name", "slug", "order"], + description: "A kollégium bemutató oldalon megjelenő kollégiumok kezelése.", + }, + fields: [ + { + name: "name", + label: "Név", + type: "text", + required: true, + }, + { + name: "slug", + label: "Slug", + type: "text", + required: true, + unique: true, + admin: { + description: "Útvonalhoz használt azonosító, pl. baross, sch.", + }, + }, + { + name: "coverImage", + label: "Borítókép", + type: "upload", + relationTo: "media", + required: true, + }, + { + name: "externalLink", + label: "Külső link", + type: "text", + required: false, + admin: { + description: "Opcionális külső oldal, ahová a Részletek gomb navigál.", + }, + validate: validateOptionalUrl, + }, + { + name: "order", + label: "Sorrend", + type: "number", + required: true, + defaultValue: 0, + }, + ], +}; diff --git a/src/components/common/ImageCard.tsx b/src/components/common/ImageCard.tsx index 57c35b7..07b86d1 100644 --- a/src/components/common/ImageCard.tsx +++ b/src/components/common/ImageCard.tsx @@ -1,44 +1,88 @@ -"use client"; -import { Card, CardContent } from "@/components/ui/card"; +import { ArrowRight } from "lucide-react"; import Image from "next/image"; +import Link from "next/link"; interface ImageCardProps { - href: string; - imageSrc: string; - title: string; - description?: string; + href: string; + imageSrc: string; + title: string; + description?: string; + detailsLabel?: string; } -export function ImageCard({ content }: Readonly<{content: ImageCardProps }> ) { - const { href, imageSrc, title, description} = content; - const shouldBeBlank = href.startsWith('http'); +export function ImageCard({ content }: Readonly<{ content: ImageCardProps }>) { + const { + href, + imageSrc, + title, + description, + detailsLabel = "Részletek", + } = content; + const isExternal = href.startsWith("http"); + + const card = ( +
+
+ ); + + if (isExternal) { return ( - - - -
-
-
- {title} -
-
- -
-

- {title} -

-

{description}

-
-
-
-
-
- ) - -} \ No newline at end of file + + {card} + + ); + } + + return ( + + {card} + + ); +} diff --git a/src/dictionaries/en/dormitories.json b/src/dictionaries/en/dormitories.json index 5243cb6..922a9b0 100644 --- a/src/dictionaries/en/dormitories.json +++ b/src/dictionaries/en/dormitories.json @@ -14,7 +14,9 @@ "contacts_p2": "For general inquiries, feel free to contact us at ", "contacts_p3": ".", "dormitory": "Dormitory", - "introduction": "Dormitory introduction" + "introduction": "Dormitory overview", + "details": "Details", + "no_results": "There are currently no dormitories to display." } } -} \ No newline at end of file +} diff --git a/src/dictionaries/hu/dormitories.json b/src/dictionaries/hu/dormitories.json index 9ba4199..18c5ef2 100644 --- a/src/dictionaries/hu/dormitories.json +++ b/src/dictionaries/hu/dormitories.json @@ -14,7 +14,9 @@ "contacts_p2": "Általános ügyekben forduljatok bátran hozzánk az ", "contacts_p3": " elérhetőségen.", "dormitory": "Kollégium", - "introduction": "Kollégium bemutatók" + "introduction": "Kollégiumok bemutatója", + "details": "Részletek", + "no_results": "Jelenleg nincs megjeleníthető kollégium." } } -} \ No newline at end of file +} diff --git a/src/lib/payload-cms.ts b/src/lib/payload-cms.ts index 7704c8b..9664a1f 100644 --- a/src/lib/payload-cms.ts +++ b/src/lib/payload-cms.ts @@ -2,6 +2,7 @@ import { AcademicScholarshipFaq, Club, Decision, + Dormitory, Event, EhkEvent, EhkScholarship, @@ -128,6 +129,18 @@ export async function getDormitoryRegulations() { return regulations.docs as Regulation[]; } +export async function getDormitories() { + const payload = await getPayload({ config }); + const dormitories = await payload.find({ + collection: "dormitories", + limit: 1000, + sort: "order", + depth: 1, + }); + + return dormitories.docs as Dormitory[]; +} + export async function getNewsById(id: number) { const payload = await getPayload({ config }); const news = await payload.find({ diff --git a/src/migrations/20260703_140815_dormitories_collection.ts b/src/migrations/20260703_140815_dormitories_collection.ts new file mode 100644 index 0000000..29f5d29 --- /dev/null +++ b/src/migrations/20260703_140815_dormitories_collection.ts @@ -0,0 +1,40 @@ +import { MigrateDownArgs, MigrateUpArgs, sql } from "@payloadcms/db-postgres"; + +export async function up({ db }: MigrateUpArgs): Promise { + await db.execute(sql` + CREATE TABLE "dormitories" ( + "id" serial PRIMARY KEY NOT NULL, + "name" varchar NOT NULL, + "slug" varchar NOT NULL, + "cover_image_id" integer NOT NULL, + "external_link" varchar, + "order" numeric DEFAULT 0 NOT NULL, + "updated_at" timestamp(3) with time zone DEFAULT now() NOT NULL, + "created_at" timestamp(3) with time zone DEFAULT now() NOT NULL + ); + + ALTER TABLE "payload_locked_documents_rels" ADD COLUMN "dormitories_id" integer; + ALTER TABLE "dormitories" ADD CONSTRAINT "dormitories_cover_image_id_media_id_fk" FOREIGN KEY ("cover_image_id") REFERENCES "public"."media"("id") ON DELETE set null ON UPDATE no action; + ALTER TABLE "payload_locked_documents_rels" ADD CONSTRAINT "payload_locked_documents_rels_dormitories_fk" FOREIGN KEY ("dormitories_id") REFERENCES "public"."dormitories"("id") ON DELETE cascade ON UPDATE no action; + CREATE UNIQUE INDEX "dormitories_slug_idx" ON "dormitories" USING btree ("slug"); + CREATE INDEX "dormitories_cover_image_idx" ON "dormitories" USING btree ("cover_image_id"); + CREATE INDEX "dormitories_order_idx" ON "dormitories" USING btree ("order"); + CREATE INDEX "dormitories_updated_at_idx" ON "dormitories" USING btree ("updated_at"); + CREATE INDEX "dormitories_created_at_idx" ON "dormitories" USING btree ("created_at"); + CREATE INDEX "payload_locked_documents_rels_dormitories_id_idx" ON "payload_locked_documents_rels" USING btree ("dormitories_id");`); +} + +export async function down({ db }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE "payload_locked_documents_rels" DROP CONSTRAINT "payload_locked_documents_rels_dormitories_fk"; + ALTER TABLE "dormitories" DROP CONSTRAINT "dormitories_cover_image_id_media_id_fk"; + DROP INDEX "payload_locked_documents_rels_dormitories_id_idx"; + DROP INDEX "dormitories_created_at_idx"; + DROP INDEX "dormitories_updated_at_idx"; + DROP INDEX "dormitories_order_idx"; + DROP INDEX "dormitories_cover_image_idx"; + DROP INDEX "dormitories_slug_idx"; + ALTER TABLE "payload_locked_documents_rels" DROP COLUMN "dormitories_id"; + ALTER TABLE "dormitories" DISABLE ROW LEVEL SECURITY; + DROP TABLE "dormitories" CASCADE;`); +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 567a50b..9c254d3 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -26,6 +26,7 @@ import * as migration_20260619_141957_add_permissions_submission_and_external_li import * as migration_20260619_173736_academic_scholarship_faq from './20260619_173736_academic_scholarship_faq'; import * as migration_20260620_123301 from './20260620_123301'; import * as migration_20260620_151905_ehk_scholarships from './20260620_151905_ehk_scholarships'; +import * as migration_20260703_140815_dormitories_collection from './20260703_140815_dormitories_collection'; export const migrations = [ { @@ -168,4 +169,9 @@ export const migrations = [ down: migration_20260620_151905_ehk_scholarships.down, name: '20260620_151905_ehk_scholarships', }, + { + up: migration_20260703_140815_dormitories_collection.up, + down: migration_20260703_140815_dormitories_collection.down, + name: '20260703_140815_dormitories_collection', + }, ]; diff --git a/src/payload-types.ts b/src/payload-types.ts index 533e368..f24b6fd 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -69,6 +69,7 @@ export interface Config { collections: { users: User; clubs: Club; + dormitories: Dormitory; media: Media; representatives: Representative; reminders: Reminder; @@ -93,6 +94,7 @@ export interface Config { collectionsSelect: { users: UsersSelect | UsersSelect; clubs: ClubsSelect | ClubsSelect; + dormitories: DormitoriesSelect | DormitoriesSelect; media: MediaSelect | MediaSelect; representatives: RepresentativesSelect | RepresentativesSelect; reminders: RemindersSelect | RemindersSelect; @@ -301,6 +303,28 @@ export interface Media { focalX?: number | null; focalY?: number | null; } +/** + * A kollégium bemutató oldalon megjelenő kollégiumok kezelése. + * + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "dormitories". + */ +export interface Dormitory { + id: number; + name: string; + /** + * Útvonalhoz használt azonosító, pl. baross, sch. + */ + slug: string; + coverImage: number | Media; + /** + * Opcionális külső oldal, ahová a Részletek gomb navigál. + */ + externalLink?: string | null; + order: number; + updatedAt: string; + createdAt: string; +} /** * Képviselők adatainak kezelése. Beszámolók feltöltése. * @@ -915,6 +939,10 @@ export interface PayloadLockedDocument { relationTo: 'clubs'; value: number | Club; } | null) + | ({ + relationTo: 'dormitories'; + value: number | Dormitory; + } | null) | ({ relationTo: 'media'; value: number | Media; @@ -1070,6 +1098,19 @@ export interface ClubsSelect { updatedAt?: T; createdAt?: T; } +/** + * This interface was referenced by `Config`'s JSON-Schema + * via the `definition` "dormitories_select". + */ +export interface DormitoriesSelect { + name?: T; + slug?: T; + coverImage?: T; + externalLink?: T; + order?: T; + updatedAt?: T; + createdAt?: T; +} /** * This interface was referenced by `Config`'s JSON-Schema * via the `definition` "media_select". diff --git a/src/payload.config.ts b/src/payload.config.ts index 28df352..24f6cf3 100644 --- a/src/payload.config.ts +++ b/src/payload.config.ts @@ -9,6 +9,7 @@ import { fileURLToPath } from "url"; import { Clubs } from "@/collections/Clubs"; import { Decisions } from "@/collections/Decisions"; +import { Dormitories } from "@/collections/Dormitories"; import { Events } from "@/collections/Events"; import { EhkEvents } from "@/collections/EhkEvents"; import { EhkScholarships } from "@/collections/EhkScholarships"; @@ -46,6 +47,7 @@ export default buildConfig({ collections: [ Users, Clubs, + Dormitories, Media, Representatives, Reminders, From 971362963131f3f69364df2e5bacb2b04e653ded Mon Sep 17 00:00:00 2001 From: DankaMarci Date: Fri, 3 Jul 2026 14:57:10 +0200 Subject: [PATCH 2/2] feat: implement redesigned dormitory details page with CMS integration and extend dormitory collection --- .../kollegium-bemutato/[slug]/page.tsx | 449 ++++++++++++------ .../kollegium/kollegium-bemutato/page.tsx | 54 +-- src/collections/Dormitories.ts | 114 ++++- src/lib/payload-cms.ts | 16 + ...60703_143300_extend_dormitories_details.ts | 73 +++ src/migrations/index.ts | 6 + src/payload-types.ts | 77 +++ 7 files changed, 583 insertions(+), 206 deletions(-) create mode 100644 src/migrations/20260703_143300_extend_dormitories_details.ts diff --git a/src/app/(app)/[lang]/kollegium/kollegium-bemutato/[slug]/page.tsx b/src/app/(app)/[lang]/kollegium/kollegium-bemutato/[slug]/page.tsx index 42eb138..d06b908 100644 --- a/src/app/(app)/[lang]/kollegium/kollegium-bemutato/[slug]/page.tsx +++ b/src/app/(app)/[lang]/kollegium/kollegium-bemutato/[slug]/page.tsx @@ -1,166 +1,307 @@ +export const dynamic = "force-dynamic"; + import { PageHeader } from "@/components/common/PageHeader"; -import { getDictionary } from "@/get-dictionary"; -import { Locale } from "@/i18n-config"; -import { notFound } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import { i18n, Locale } from "@/i18n-config"; +import { getDormitoryBySlug } from "@/lib/payload-cms"; +import type { Dormitory, Media } from "@/payload-types"; +import type { SerializedEditorState } from "@payloadcms/richtext-lexical/lexical"; +import { RichText } from "@payloadcms/richtext-lexical/react"; +import { + BedDouble, + ExternalLink, + GraduationCap, + MapPin, + Users, +} from "lucide-react"; import Link from "next/link"; -import { ArrowLeft } from "lucide-react"; -import fs from "fs"; -import path from "path"; - -const validSlugs = ["baross", "bercsenyi", "karman", "martos", "sch", "vpk", "wigner"] as const; -type DormitorySlug = typeof validSlugs[number]; - -type ContentBlock = - | { type: 'text'; content: string } - | { type: 'images'; indices: number[] }; - -function getDormitoryImages(slug: DormitorySlug): string[] { - const imagesDir = path.join(process.cwd(), "public", "kolik", slug); - try { - if (!fs.existsSync(imagesDir)) { - return []; - } - const files = fs.readdirSync(imagesDir); - return files - .filter(file => /\.(jpg|jpeg|png|gif|webp)$/i.test(file)) - .sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })) - .map(file => encodeURI(`/kolik/${slug}/${file}`)); - } catch (e) { - console.error("Failed to read dormitory images directory", e); - return []; - } -} +import { notFound } from "next/navigation"; -export async function generateStaticParams() { - return validSlugs.map((slug) => ({ - slug, - })); -} +const labels = { + hu: { + back: "Vissza", + description: "Leírás", + gallery: "Galéria", + noDescription: "Ehhez a kollégiumhoz még nincs feltöltve leírás.", + noGallery: "Ehhez a kollégiumhoz még nincs feltöltve galéria.", + capacity: "FÉRŐHELYEK", + address: "CÍM", + roomInfo: "SZOBÁK", + targetAudience: "KIK KAPHATNAK ITT HELYET?", + website: "WEBOLDAL", + }, + en: { + back: "Back", + description: "Description", + gallery: "Gallery", + noDescription: "No description has been uploaded for this dormitory yet.", + noGallery: "No gallery has been uploaded for this dormitory yet.", + capacity: "CAPACITY", + address: "ADDRESS", + roomInfo: "ROOMS", + targetAudience: "WHO CAN GET A PLACE HERE?", + website: "WEBSITE", + }, +} as const; + +const hasRichText = (data: unknown): data is SerializedEditorState => { + return Boolean(data && typeof data === "object" && "root" in data); +}; + +const getLocalizedText = ( + item: object, + field: string, + locale: Locale, +) => { + const source = item as Record; + const primary = source[`${field}_${locale}`]; + const fallback = source[`${field}_${locale === "en" ? "hu" : "en"}`]; + + return typeof primary === "string" && primary.trim() + ? primary + : typeof fallback === "string" && fallback.trim() + ? fallback + : ""; +}; + +const getLocalizedRichText = (dormitory: Dormitory, locale: Locale) => { + const primary = dormitory[`description_${locale}` as keyof Dormitory]; + const fallback = + dormitory[`description_${locale === "en" ? "hu" : "en"}` as keyof Dormitory]; + + if (hasRichText(primary)) { + return primary; + } + + return hasRichText(fallback) ? fallback : null; +}; + +const getMedia = (image: number | Media | null | undefined) => { + return typeof image === "object" && image !== null ? image : null; +}; export default async function DormitoryDetailsPage({ - params + params, }: Readonly<{ params: Promise<{ lang: Locale; slug: string }> }>) { - const { lang, slug } = await params; - - if (!validSlugs.includes(slug as DormitorySlug)) { - notFound(); - } - - const typedSlug = slug as DormitorySlug; - const dictionary = await getDictionary(lang, 'dormitory_details'); - const detailsDict = dictionary.dormitory_details; - const dormData = detailsDict[typedSlug]; - const images = getDormitoryImages(typedSlug); - - // Split at (Image) ignoring leading/trailing newlines - const descriptionParts = dormData.description.split(/(?:\r?\n)*\(Image\)(?:\r?\n)*/i); - const embeddedImageCount = Math.min(images.length, descriptionParts.length - 1); - const remainingImages = images.slice(embeddedImageCount); - - const blocks: ContentBlock[] = []; - let currentImageGroup: number[] = []; - - descriptionParts.forEach((part: string, idx: number) => { - const text = part.trim(); - if (text) { - if (currentImageGroup.length > 0) { - blocks.push({ type: 'images', indices: currentImageGroup }); - currentImageGroup = []; - } - blocks.push({ type: 'text', content: text }); - } - - if (idx < descriptionParts.length - 1) { - currentImageGroup.push(idx); - } - }); - - if (currentImageGroup.length > 0) { - blocks.push({ type: 'images', indices: currentImageGroup }); - } - - return ( -
-
- - - {detailsDict.back_button} - - - - -
-
- {blocks.map((block, bIdx) => { - if (block.type === 'text') { - return ( -

- {block.content} -

- ); - } else { - return ( -
- {block.indices.map(imgIdx => ( - images[imgIdx] ? ( - {`${dormData.title} 1 - ? "max-w-full sm:max-w-[calc(50%-12px)] md:max-w-[calc(33.333%-16px)] max-h-[40vh]" - : "max-w-full max-h-[60vh]" - }`} - loading="lazy" - /> - ) : ( -
1 - ? "max-w-full sm:max-w-[calc(50%-12px)] md:max-w-[calc(33.333%-16px)]" - : "w-full" - }`}> - [ Kép helye: {imgIdx + 1}. kép hiányzik ] -
- ) - ))} -
- ); - } - })} + const { lang, slug } = await params; + const validLang = i18n.locales.includes(lang) ? lang : i18n.defaultLocale; + const t = labels[validLang]; + const dormitory = await getDormitoryBySlug(slug); + + if (!dormitory) { + notFound(); + } + + const description = getLocalizedRichText(dormitory, validLang); + const gallery = + dormitory.gallery + ?.map((category) => ({ + id: category.id, + name: getLocalizedText(category, "categoryName", validLang), + images: + category.images + ?.map((entry) => getMedia(entry.image)) + .filter((image): image is Media => Boolean(image?.url)) ?? [], + })) + .filter((category) => category.images.length > 0) ?? []; + + return ( +
+
+ + +
+
+
+
+

+ {t.description} +

+
+ {description ? ( +
+
+ ) : ( +

+ {t.noDescription} +

+ )} +
+
+ +
+
+

+ {t.gallery} +

+ {gallery.length > 1 && ( + - {remainingImages.length > 0 || images.length === 0 ? ( -
-

- {detailsDict.images_title} -

- - {remainingImages.length > 0 ? ( -
- {remainingImages.map((imgSrc, idx) => ( -
- {`${dormData.title} -
- ))} -
- ) : ( -
- {detailsDict.no_images} -
- )} + {gallery.length > 0 ? ( +
+
+ {gallery.map((category, index) => ( +
+

+ {category.name || t.gallery} +

+
+ {category.images.map((image, imageIndex) => ( +
+ { +
+ ))} +
+
+ ))}
- ) : null} +
+ ) : ( +
+ {t.noGallery} +
+ )} +
+
+ + +
+
+
+
+ ); +} + +function DormitorySidebar({ + dormitory, + locale, +}: { + dormitory: Dormitory; + locale: Locale; +}) { + const t = labels[locale]; + const address = getLocalizedText(dormitory, "address", locale); + const roomInfo = getLocalizedText(dormitory, "roomInfo", locale); + const targetAudience = getLocalizedText(dormitory, "targetAudience", locale); + const items = [ + dormitory.capacity + ? { + icon: Users, + label: t.capacity, + value: String(dormitory.capacity), + } + : null, + address + ? { + icon: MapPin, + label: t.address, + value: address, + href: dormitory.mapUrl || undefined, + } + : null, + roomInfo + ? { + icon: BedDouble, + label: t.roomInfo, + value: roomInfo, + } + : null, + targetAudience + ? { + icon: GraduationCap, + label: t.targetAudience, + value: targetAudience, + } + : null, + ].filter(Boolean); + + return ( +
+ + ); } diff --git a/src/app/(app)/[lang]/kollegium/kollegium-bemutato/page.tsx b/src/app/(app)/[lang]/kollegium/kollegium-bemutato/page.tsx index 2aec70d..100dbdb 100644 --- a/src/app/(app)/[lang]/kollegium/kollegium-bemutato/page.tsx +++ b/src/app/(app)/[lang]/kollegium/kollegium-bemutato/page.tsx @@ -7,44 +7,6 @@ import { getDormitories } from "@/lib/payload-cms"; import { Media } from "@/payload-types"; import { ImageCard } from "@/components/common/ImageCard"; -const fallbackDormitories = [ - { - name: "Baross Gábor Kollégium", - slug: "baross", - imageSrc: "/kolik/baross.jpg", - }, - { - name: "Bercsényi 28-30 Kollégium", - slug: "bercsenyi", - imageSrc: "/kolik/bercsenyi.jpg", - }, - { - name: "Kármán Tódor Kollégium", - slug: "karman", - imageSrc: "/kolik/karman.jpg", - }, - { - name: "Martos Kollégium", - slug: "martos", - imageSrc: "/kolik/martos.jpg", - }, - { - name: "Schönherz Kollégium", - slug: "sch", - imageSrc: "/kolik/schonherz.jpg", - }, - { - name: "Vásárhelyi Pál Kollégium", - slug: "vpk", - imageSrc: "/kolik/vasarhelyi.jpg", - }, - { - name: "Wigner Jenő Kollégium", - slug: "wigner", - imageSrc: "/kolik/wigner.jpg", - }, -]; - export default async function DormitoriesOverviewPage({ params }: Readonly<{ params: Promise<{ lang: Locale }> }>){ const { lang } = await params; @@ -70,29 +32,19 @@ export default async function DormitoriesOverviewPage({ name: dormitory.name, slug: dormitory.slug, imageSrc: coverImage.url, - href: - dormitory.externalLink || - `/${lang}/kollegium/kollegium-bemutato/${dormitory.slug}`, + href: `/${lang}/kollegium/kollegium-bemutato/${dormitory.slug}`, }; }) .filter((card): card is { name: string; slug: string; imageSrc: string; href: string } => card !== null); - const renderedCards = - cards.length > 0 - ? cards - : fallbackDormitories.map((dormitory) => ({ - ...dormitory, - href: `/${lang}/kollegium/kollegium-bemutato/${dormitory.slug}`, - })); - return (
- {renderedCards.length > 0 ? ( + {cards.length > 0 ? (
- {renderedCards.map((dormitory) => ( + {cards.map((dormitory) => ( { if (!val) return true; @@ -25,7 +26,7 @@ export const Dormitories: CollectionConfig = { }, admin: { useAsTitle: "name", - defaultColumns: ["name", "slug", "order"], + defaultColumns: ["name", "slug", "capacity", "order"], description: "A kollégium bemutató oldalon megjelenő kollégiumok kezelése.", }, fields: [ @@ -62,6 +63,117 @@ export const Dormitories: CollectionConfig = { }, validate: validateOptionalUrl, }, + { + name: "description_hu", + label: "Leírás (magyar)", + type: "richText", + required: false, + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [ + ...defaultFeatures, + FixedToolbarFeature(), + ], + }), + }, + { + name: "description_en", + label: "Leírás (angol)", + type: "richText", + required: false, + editor: lexicalEditor({ + features: ({ defaultFeatures }) => [ + ...defaultFeatures, + FixedToolbarFeature(), + ], + }), + }, + { + name: "capacity", + label: "Férőhelyek száma", + type: "number", + required: false, + }, + { + name: "address_hu", + label: "Cím (magyar)", + type: "text", + required: false, + }, + { + name: "address_en", + label: "Cím (angol)", + type: "text", + required: false, + }, + { + name: "mapUrl", + label: "Google Maps link", + type: "text", + required: false, + admin: { + description: "Opcionális Google Maps vagy térkép link a címhez.", + }, + validate: validateOptionalUrl, + }, + { + name: "roomInfo_hu", + label: "Szobainformáció (magyar)", + type: "text", + required: false, + }, + { + name: "roomInfo_en", + label: "Szobainformáció (angol)", + type: "text", + required: false, + }, + { + name: "targetAudience_hu", + label: "Célközönség (magyar)", + type: "text", + required: false, + }, + { + name: "targetAudience_en", + label: "Célközönség (angol)", + type: "text", + required: false, + }, + { + name: "gallery", + label: "Kategorizált galéria", + type: "array", + required: false, + fields: [ + { + name: "categoryName_hu", + label: "Kategória neve (magyar)", + type: "text", + required: true, + }, + { + name: "categoryName_en", + label: "Kategória neve (angol)", + type: "text", + required: false, + }, + { + name: "images", + label: "Képek", + type: "array", + required: false, + fields: [ + { + name: "image", + label: "Kép", + type: "upload", + relationTo: "media", + required: true, + }, + ], + }, + ], + }, { name: "order", label: "Sorrend", diff --git a/src/lib/payload-cms.ts b/src/lib/payload-cms.ts index 9664a1f..f5fa70e 100644 --- a/src/lib/payload-cms.ts +++ b/src/lib/payload-cms.ts @@ -141,6 +141,22 @@ export async function getDormitories() { return dormitories.docs as Dormitory[]; } +export async function getDormitoryBySlug(slug: string) { + const payload = await getPayload({ config }); + const dormitory = await payload.find({ + collection: "dormitories", + depth: 2, + limit: 1, + where: { + slug: { + equals: slug, + }, + }, + }); + + return dormitory.docs[0] as Dormitory | undefined; +} + export async function getNewsById(id: number) { const payload = await getPayload({ config }); const news = await payload.find({ diff --git a/src/migrations/20260703_143300_extend_dormitories_details.ts b/src/migrations/20260703_143300_extend_dormitories_details.ts new file mode 100644 index 0000000..e4534f0 --- /dev/null +++ b/src/migrations/20260703_143300_extend_dormitories_details.ts @@ -0,0 +1,73 @@ +import { MigrateDownArgs, MigrateUpArgs, sql } from "@payloadcms/db-postgres"; + +export async function up({ db }: MigrateUpArgs): Promise { + await db.execute(sql` + ALTER TABLE "dormitories" ADD COLUMN IF NOT EXISTS "description_hu" jsonb; + ALTER TABLE "dormitories" ADD COLUMN IF NOT EXISTS "description_en" jsonb; + ALTER TABLE "dormitories" ADD COLUMN IF NOT EXISTS "capacity" numeric; + ALTER TABLE "dormitories" ADD COLUMN IF NOT EXISTS "address_hu" varchar; + ALTER TABLE "dormitories" ADD COLUMN IF NOT EXISTS "address_en" varchar; + ALTER TABLE "dormitories" ADD COLUMN IF NOT EXISTS "map_url" varchar; + ALTER TABLE "dormitories" ADD COLUMN IF NOT EXISTS "room_info_hu" varchar; + ALTER TABLE "dormitories" ADD COLUMN IF NOT EXISTS "room_info_en" varchar; + ALTER TABLE "dormitories" ADD COLUMN IF NOT EXISTS "target_audience_hu" varchar; + ALTER TABLE "dormitories" ADD COLUMN IF NOT EXISTS "target_audience_en" varchar; + + CREATE TABLE IF NOT EXISTS "dormitories_gallery" ( + "_order" integer NOT NULL, + "_parent_id" integer NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "category_name_hu" varchar NOT NULL, + "category_name_en" varchar + ); + + CREATE TABLE IF NOT EXISTS "dormitories_gallery_images" ( + "_order" integer NOT NULL, + "_parent_id" varchar NOT NULL, + "id" varchar PRIMARY KEY NOT NULL, + "image_id" integer NOT NULL + ); + + DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'dormitories_gallery_parent_id_fk') THEN + ALTER TABLE "dormitories_gallery" ADD CONSTRAINT "dormitories_gallery_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."dormitories"("id") ON DELETE cascade ON UPDATE no action; + END IF; + END $$; + + DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'dormitories_gallery_images_parent_id_fk') THEN + ALTER TABLE "dormitories_gallery_images" ADD CONSTRAINT "dormitories_gallery_images_parent_id_fk" FOREIGN KEY ("_parent_id") REFERENCES "public"."dormitories_gallery"("id") ON DELETE cascade ON UPDATE no action; + END IF; + END $$; + + DO $$ BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'dormitories_gallery_images_image_id_media_id_fk') THEN + ALTER TABLE "dormitories_gallery_images" ADD CONSTRAINT "dormitories_gallery_images_image_id_media_id_fk" FOREIGN KEY ("image_id") REFERENCES "public"."media"("id") ON DELETE cascade ON UPDATE no action; + END IF; + END $$; + + CREATE INDEX IF NOT EXISTS "dormitories_gallery_order_idx" ON "dormitories_gallery" USING btree ("_order"); + CREATE INDEX IF NOT EXISTS "dormitories_gallery_parent_id_idx" ON "dormitories_gallery" USING btree ("_parent_id"); + CREATE INDEX IF NOT EXISTS "dormitories_gallery_images_order_idx" ON "dormitories_gallery_images" USING btree ("_order"); + CREATE INDEX IF NOT EXISTS "dormitories_gallery_images_parent_id_idx" ON "dormitories_gallery_images" USING btree ("_parent_id"); + CREATE INDEX IF NOT EXISTS "dormitories_gallery_images_image_idx" ON "dormitories_gallery_images" USING btree ("image_id");`); +} + +export async function down({ db }: MigrateDownArgs): Promise { + await db.execute(sql` + ALTER TABLE IF EXISTS "dormitories_gallery_images" DISABLE ROW LEVEL SECURITY; + ALTER TABLE IF EXISTS "dormitories_gallery" DISABLE ROW LEVEL SECURITY; + DROP TABLE IF EXISTS "dormitories_gallery_images" CASCADE; + DROP TABLE IF EXISTS "dormitories_gallery" CASCADE; + + ALTER TABLE "dormitories" DROP COLUMN IF EXISTS "target_audience_en"; + ALTER TABLE "dormitories" DROP COLUMN IF EXISTS "target_audience_hu"; + ALTER TABLE "dormitories" DROP COLUMN IF EXISTS "room_info_en"; + ALTER TABLE "dormitories" DROP COLUMN IF EXISTS "room_info_hu"; + ALTER TABLE "dormitories" DROP COLUMN IF EXISTS "map_url"; + ALTER TABLE "dormitories" DROP COLUMN IF EXISTS "address_en"; + ALTER TABLE "dormitories" DROP COLUMN IF EXISTS "address_hu"; + ALTER TABLE "dormitories" DROP COLUMN IF EXISTS "capacity"; + ALTER TABLE "dormitories" DROP COLUMN IF EXISTS "description_en"; + ALTER TABLE "dormitories" DROP COLUMN IF EXISTS "description_hu";`); +} diff --git a/src/migrations/index.ts b/src/migrations/index.ts index 9c254d3..2fac29e 100644 --- a/src/migrations/index.ts +++ b/src/migrations/index.ts @@ -27,6 +27,7 @@ import * as migration_20260619_173736_academic_scholarship_faq from './20260619_ import * as migration_20260620_123301 from './20260620_123301'; import * as migration_20260620_151905_ehk_scholarships from './20260620_151905_ehk_scholarships'; import * as migration_20260703_140815_dormitories_collection from './20260703_140815_dormitories_collection'; +import * as migration_20260703_143300_extend_dormitories_details from './20260703_143300_extend_dormitories_details'; export const migrations = [ { @@ -174,4 +175,9 @@ export const migrations = [ down: migration_20260703_140815_dormitories_collection.down, name: '20260703_140815_dormitories_collection', }, + { + up: migration_20260703_143300_extend_dormitories_details.up, + down: migration_20260703_143300_extend_dormitories_details.down, + name: '20260703_143300_extend_dormitories_details', + }, ]; diff --git a/src/payload-types.ts b/src/payload-types.ts index f24b6fd..7ec9edf 100644 --- a/src/payload-types.ts +++ b/src/payload-types.ts @@ -321,6 +321,60 @@ export interface Dormitory { * Opcionális külső oldal, ahová a Részletek gomb navigál. */ externalLink?: string | null; + description_hu?: { + root: { + type: string; + children: { + type: any; + version: number; + [k: string]: unknown; + }[]; + direction: ('ltr' | 'rtl') | null; + format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; + description_en?: { + root: { + type: string; + children: { + type: any; + version: number; + [k: string]: unknown; + }[]; + direction: ('ltr' | 'rtl') | null; + format: 'left' | 'start' | 'center' | 'right' | 'end' | 'justify' | ''; + indent: number; + version: number; + }; + [k: string]: unknown; + } | null; + capacity?: number | null; + address_hu?: string | null; + address_en?: string | null; + /** + * Opcionális Google Maps vagy térkép link a címhez. + */ + mapUrl?: string | null; + roomInfo_hu?: string | null; + roomInfo_en?: string | null; + targetAudience_hu?: string | null; + targetAudience_en?: string | null; + gallery?: + | { + categoryName_hu: string; + categoryName_en?: string | null; + images?: + | { + image: number | Media; + id?: string | null; + }[] + | null; + id?: string | null; + }[] + | null; order: number; updatedAt: string; createdAt: string; @@ -1107,6 +1161,29 @@ export interface DormitoriesSelect { slug?: T; coverImage?: T; externalLink?: T; + description_hu?: T; + description_en?: T; + capacity?: T; + address_hu?: T; + address_en?: T; + mapUrl?: T; + roomInfo_hu?: T; + roomInfo_en?: T; + targetAudience_hu?: T; + targetAudience_en?: T; + gallery?: + | T + | { + categoryName_hu?: T; + categoryName_en?: T; + images?: + | T + | { + image?: T; + id?: T; + }; + id?: T; + }; order?: T; updatedAt?: T; createdAt?: T;