feat(frontend): Payload Local API + hu/en locale routing (MITHOME-91/114)
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
CI Pipeline with Test Management / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI Pipeline with Test Management / 🐳 Docker Integration Tests (push) Blocked by required conditions
CI Pipeline with Test Management / 🏗️ Build Docker Image (push) Blocked by required conditions
CI Pipeline with Test Management / 📊 Generate Test Summary (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🧪 Run Tests & Generate Reports (push) Waiting to run
Test Reporting & Gherkin Analysis / 📊 Analyze Test Coverage (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / 🔄 Sync with Linear (push) Blocked by required conditions
Test Reporting & Gherkin Analysis / ⚡ Performance Monitoring (push) Blocked by required conditions
Replaces the JSON content system with Payload's Local API across every
frontend page, and introduces symmetric locale-prefixed routing
(/hu/..., /en/...) with per-locale translated slugs — supersedes the
earlier "hu unprefixed" decision (see chat 2026-09-10).
Routing structure:
- src/app/(frontend)/layout.tsx: now a minimal shell (html/body, theme
script, ThemeProvider) — no longer locale-aware.
- src/app/(frontend)/page.tsx: redirects bare "/" to the default
locale (/hu).
- src/app/(frontend)/not-found.tsx: explicit 404 for the (frontend)
group — without it, Next's built-in fallback collided with the
(payload) group's own root and reproduced the "double html / script
tag" symptom from MITHOME-87, but only on notFound() paths. Verified
fixed in both dev and a real production (standalone) server; the
remaining "script tag" console warning on invalid routes turned out
to be Turbopack dev-mode-only noise (zero console errors in
production) — confirmed by building and running .next/standalone
directly.
- src/app/(frontend)/[locale]/layout.tsx: validates the locale segment
(generateStaticParams hu/en, notFound() otherwise), fetches Common +
Home via Payload, renders Header/Footer/staging-banner.
- src/app/(frontend)/[locale]/page.tsx: home, fetches Home global +
Partners collection.
- src/app/(frontend)/[locale]/[slug]/page.tsx: catch-all for about/
services/contact/privacy/terms — resolves slug -> PageKey via
src/lib/i18n.ts's PAGE_SLUGS map (generateStaticParams pre-renders
all 10 locale×slug combinations), generateMetadata per page.
New lib layer:
- src/lib/i18n.ts: Locale/PageKey types, PAGE_SLUGS (translated slugs
per locale), localePath()/resolvePageKey()/switchLocalePath()
helpers (the last one already shaped for MITHOME-115).
- src/lib/payload-content.ts: Local API getters that also unwrap
Payload's `{ value: string }[]` array-field shape back into plain
string[] (see src/globals/fields/stringArray.ts) — keeps the page
JSX consuming the exact shape the old content/types.ts had, so the
migration is a data-source swap, not a markup rewrite.
Presentational split: page bodies moved to src/components/views/
(HomeView, AboutView, ServicesView, ContactView, LegalPageView — the
last one shared by both legal pages, identical shape) as prop-driven
components; the app-router page.tsx files became thin server-side
fetch + render wrappers. Header/Footer converted from importing
content directly to accepting nav/locale/content props, since they're
'use client' and can't call the Payload Local API themselves —
config/site.ts's navigation arrays became getMainNavigation(locale)/
getFooterNavigation(locale)/getFooterLegalLinks(locale) functions.
Two real, pre-existing bugs fixed along the way (not introduced by
this migration):
- Services and Contact pages' "Webmail belépés" links used the
primary CTA's href (/kapcsolat) with target="_blank" instead of the
actual webmail URL (home.hero.cta.secondary.href) — now correct.
- The GDPR checkbox link pointed to "/adatkezelesi-tajekoztato", which
never matched the real privacy page route under any past URL
scheme. contact.json's gdpr.label now carries a {privacyHref}
placeholder that ContactView replaces with the locale-correct path
— also fixes the adatvedelem page's own <title> tag, which
previously read "Adatvédelmi Tájékoztató | Szolgáltatás jellemzők:"
(a copy-paste bug using common.labels.features instead of the site
name).
Known, accepted limitation: the outer shell layout hardcodes
<html lang="hu"> because it sits above the [locale] segment and can't
read the param — every [locale]/[slug] page's own generateMetadata is
locale-correct, but the initial lang attribute isn't. Documented as a
MITHOME-116 (SEO/hreflang) follow-up rather than restructured now.
Verified end to end in a real browser: /hu matches the
https://stage.mozdit.hu visual baseline (MITHOME-117) exactly; /en
renders with English nav/metadata (content body still Hungarian-only,
as expected — MITHOME-111/113 not done yet); /hu/kapcsolat's GDPR link
resolves to /hu/adatvedelem; dark mode still works; invalid locale
(/fr/about) and invalid slug (/hu/nemletezo-oldal) both 404 correctly;
bare "/" redirects to /hu. build/lint/tsc/test (58 passed) all clean,
including a clean production standalone-server run.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
594865ea9a
commit
d6f3dda9e5
@@ -0,0 +1,128 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
import AboutView from '@/components/views/AboutView'
|
||||||
|
import ServicesView from '@/components/views/ServicesView'
|
||||||
|
import ContactView from '@/components/views/ContactView'
|
||||||
|
import LegalPageView from '@/components/views/LegalPageView'
|
||||||
|
import { LOCALES, PAGE_SLUGS, resolvePageKey, isLocale, type Locale, type PageKey } from '@/lib/i18n'
|
||||||
|
import {
|
||||||
|
getAboutContent,
|
||||||
|
getServicesContent,
|
||||||
|
getContactContent,
|
||||||
|
getHomeContent,
|
||||||
|
getCommonContent,
|
||||||
|
getLegalPage,
|
||||||
|
} from '@/lib/payload-content'
|
||||||
|
import { siteConfig, getOgLocale } from '@/config/site'
|
||||||
|
|
||||||
|
type Params = { locale: string; slug: string }
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return LOCALES.flatMap((locale) =>
|
||||||
|
(Object.keys(PAGE_SLUGS) as PageKey[]).map((key) => ({
|
||||||
|
locale,
|
||||||
|
slug: PAGE_SLUGS[key][locale],
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const LEGAL_META: Record<'privacy' | 'terms', Record<Locale, string>> = {
|
||||||
|
privacy: {
|
||||||
|
hu: 'Adatvédelmi tájékoztató - ismerje meg, hogyan kezeljük személyes adatait.',
|
||||||
|
en: 'Privacy policy — learn how we handle your personal data.',
|
||||||
|
},
|
||||||
|
terms: {
|
||||||
|
hu: 'Általános Szerződési Feltételek - Ismerje meg a mozdIT Bt. szolgáltatásainak használati feltételeit.',
|
||||||
|
en: 'Terms of Service — learn about the terms and conditions of mozdIT Bt.’s services.',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
|
||||||
|
const { locale: rawLocale, slug } = await params
|
||||||
|
if (!isLocale(rawLocale)) return {}
|
||||||
|
const locale: Locale = rawLocale
|
||||||
|
const key = resolvePageKey(locale, slug)
|
||||||
|
if (!key) return {}
|
||||||
|
|
||||||
|
const base = (title: string, description: string, ogDescription = description) => ({
|
||||||
|
title: `${title} | ${siteConfig.general.name}`,
|
||||||
|
description,
|
||||||
|
openGraph: {
|
||||||
|
title: `${title} | ${siteConfig.general.name}`,
|
||||||
|
description: ogDescription,
|
||||||
|
url: `${siteConfig.general.url}${'/' + locale}/${slug}`,
|
||||||
|
locale: getOgLocale(locale),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (key === 'about') {
|
||||||
|
const about = await getAboutContent(locale)
|
||||||
|
return base(about.meta.title, about.meta.description, about.meta.ogDescription)
|
||||||
|
}
|
||||||
|
if (key === 'services') {
|
||||||
|
const services = await getServicesContent(locale)
|
||||||
|
return base(services.meta.title, services.meta.description, services.meta.ogDescription)
|
||||||
|
}
|
||||||
|
if (key === 'contact') {
|
||||||
|
const contact = await getContactContent(locale)
|
||||||
|
return base(contact.meta.title, contact.meta.description)
|
||||||
|
}
|
||||||
|
if (key === 'privacy') {
|
||||||
|
const page = await getLegalPage('adatvedelem', locale)
|
||||||
|
return base(page?.title ?? 'Adatvédelem', LEGAL_META.privacy[locale])
|
||||||
|
}
|
||||||
|
// terms
|
||||||
|
const page = await getLegalPage('hasznalati-feltetelek', locale)
|
||||||
|
return base(page?.title ?? 'ÁSZF', LEGAL_META.terms[locale])
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function CatchAllPage({ params }: { params: Promise<Params> }) {
|
||||||
|
const { locale: rawLocale, slug } = await params
|
||||||
|
if (!isLocale(rawLocale)) notFound()
|
||||||
|
const locale: Locale = rawLocale
|
||||||
|
const key = resolvePageKey(locale, slug)
|
||||||
|
if (!key) notFound()
|
||||||
|
|
||||||
|
if (key === 'about') {
|
||||||
|
const content = await getAboutContent(locale)
|
||||||
|
return <AboutView content={content} locale={locale} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'services') {
|
||||||
|
const [content, home, common] = await Promise.all([
|
||||||
|
getServicesContent(locale),
|
||||||
|
getHomeContent(locale),
|
||||||
|
getCommonContent(locale),
|
||||||
|
])
|
||||||
|
return (
|
||||||
|
<ServicesView
|
||||||
|
content={content}
|
||||||
|
homeServices={home.services.items}
|
||||||
|
featuresLabel={common.labels.features}
|
||||||
|
webmailHref={home.hero.cta.secondary?.href ?? siteConfig.general.url}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'contact') {
|
||||||
|
const [content, home, common] = await Promise.all([
|
||||||
|
getContactContent(locale),
|
||||||
|
getHomeContent(locale),
|
||||||
|
getCommonContent(locale),
|
||||||
|
])
|
||||||
|
return (
|
||||||
|
<ContactView
|
||||||
|
content={content}
|
||||||
|
contactEmail={siteConfig.contact.email}
|
||||||
|
footerAddress={common.footer.address}
|
||||||
|
webmailHref={home.hero.cta.secondary?.href ?? siteConfig.general.url}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = await getLegalPage(key === 'privacy' ? 'adatvedelem' : 'hasznalati-feltetelek', locale)
|
||||||
|
if (!page) notFound()
|
||||||
|
return <LegalPageView page={page} locale={locale} />
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
import Header from '../../../components/Header'
|
||||||
|
import Footer from '../../../components/Footer'
|
||||||
|
import { LOCALES, isLocale, localePath, type Locale } from '@/lib/i18n'
|
||||||
|
import { getCommonContent, getHomeContent } from '@/lib/payload-content'
|
||||||
|
import { siteConfig, getMainNavigation, getFooterNavigation, getFooterLegalLinks, getSiteDescription } from '@/config/site'
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return LOCALES.map((locale) => ({ locale }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function LocaleLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
params: Promise<{ locale: string }>
|
||||||
|
}) {
|
||||||
|
const { locale: rawLocale } = await params
|
||||||
|
if (!isLocale(rawLocale)) notFound()
|
||||||
|
const locale: Locale = rawLocale
|
||||||
|
|
||||||
|
const [common, home] = await Promise.all([
|
||||||
|
getCommonContent(locale),
|
||||||
|
getHomeContent(locale),
|
||||||
|
])
|
||||||
|
|
||||||
|
const isStaging = process.env.NEXT_PUBLIC_DEPLOY_ENV === 'staging'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isStaging && (
|
||||||
|
<div className="bg-amber-400 px-4 py-2 text-center text-xs font-extrabold tracking-[0.18em] text-amber-950 sm:text-sm">
|
||||||
|
{common.staging.banner}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Header
|
||||||
|
nav={getMainNavigation(locale)}
|
||||||
|
homeHref={localePath(locale)}
|
||||||
|
a11y={common.a11y}
|
||||||
|
/>
|
||||||
|
<main className="flex-1">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<Footer
|
||||||
|
nav={getFooterNavigation(locale)}
|
||||||
|
legalLinks={getFooterLegalLinks(locale)}
|
||||||
|
homeHref={localePath(locale)}
|
||||||
|
description={getSiteDescription(locale)}
|
||||||
|
contactEmail={siteConfig.contact.email}
|
||||||
|
footerAddress={common.footer.address}
|
||||||
|
footerCopyright={common.footer.copyright}
|
||||||
|
homeServices={home.services.items.map((item) => ({ id: item.id, title: item.title, icon: item.icon }))}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
import type { Metadata } from 'next'
|
||||||
|
import HomeView from '@/components/views/HomeView'
|
||||||
|
import { isLocale, type Locale } from '@/lib/i18n'
|
||||||
|
import { getHomeContent, getPartners } from '@/lib/payload-content'
|
||||||
|
import { siteConfig, getSiteDescription, getOgLocale } from '@/config/site'
|
||||||
|
import { notFound } from 'next/navigation'
|
||||||
|
|
||||||
|
type Params = { locale: string }
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {
|
||||||
|
const { locale: rawLocale } = await params
|
||||||
|
if (!isLocale(rawLocale)) return {}
|
||||||
|
const locale: Locale = rawLocale
|
||||||
|
const description = getSiteDescription(locale)
|
||||||
|
return {
|
||||||
|
title: `${siteConfig.general.name} | ${description}`,
|
||||||
|
description,
|
||||||
|
openGraph: {
|
||||||
|
title: siteConfig.general.name,
|
||||||
|
description,
|
||||||
|
url: siteConfig.general.url,
|
||||||
|
siteName: siteConfig.general.name,
|
||||||
|
images: [{ url: siteConfig.general.ogImage, width: 1200, height: 630, alt: siteConfig.general.name }],
|
||||||
|
locale: getOgLocale(locale),
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function HomePage({ params }: { params: Promise<Params> }) {
|
||||||
|
const { locale: rawLocale } = await params
|
||||||
|
if (!isLocale(rawLocale)) notFound()
|
||||||
|
const locale: Locale = rawLocale
|
||||||
|
|
||||||
|
const [content, partners] = await Promise.all([
|
||||||
|
getHomeContent(locale),
|
||||||
|
getPartners(),
|
||||||
|
])
|
||||||
|
|
||||||
|
return <HomeView content={content} partners={partners} locale={locale} />
|
||||||
|
}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { content } from '@/content'
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: `${content.pages.hasznalatiFeltetelek.title} | ${content.common.labels.features || 'mozdIT Bt.'}`,
|
|
||||||
description: 'Általános Szerződési Feltételek - Ismerje meg a mozdIT Bt. szolgáltatásainak használati feltételeit.',
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function TermsOfService() {
|
|
||||||
const pageContent = content.pages.hasznalatiFeltetelek
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="py-20 lg:py-28 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="mb-12">
|
|
||||||
<h1
|
|
||||||
className="text-4xl md:text-5xl font-bold mb-4"
|
|
||||||
style={{ color: 'var(--color-foreground)' }}
|
|
||||||
>
|
|
||||||
{pageContent.title}
|
|
||||||
</h1>
|
|
||||||
<p
|
|
||||||
className="text-sm"
|
|
||||||
style={{ color: 'var(--color-foreground-muted)' }}
|
|
||||||
>
|
|
||||||
Utolsó frissítés: {pageContent.lastUpdated}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-12">
|
|
||||||
{pageContent.sections.map((section) => (
|
|
||||||
<section key={section.id}>
|
|
||||||
<h2
|
|
||||||
className="text-2xl font-semibold mb-4"
|
|
||||||
style={{ color: 'var(--color-foreground)' }}
|
|
||||||
>
|
|
||||||
{section.title}
|
|
||||||
</h2>
|
|
||||||
<div
|
|
||||||
className="prose max-w-none"
|
|
||||||
style={{ color: 'var(--color-foreground-muted)' }}
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: section.content
|
|
||||||
.replace(/\n\n/g, '</p><p class="mb-4">')
|
|
||||||
.replace(/• \*\*(.*?)\*\*/g, '<br/>• <strong>$1</strong>')
|
|
||||||
.replace(/^/, '<p class="mb-4">')
|
|
||||||
.replace(/$/, '</p>')
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { siteConfig } from '@/config/site'
|
|
||||||
import { content } from '@/content'
|
|
||||||
import type { Metadata } from 'next'
|
|
||||||
|
|
||||||
const { contact: pageContent } = content.pages
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
|
||||||
title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
|
|
||||||
description: pageContent.meta.description,
|
|
||||||
openGraph: {
|
|
||||||
title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
|
|
||||||
description: pageContent.meta.description,
|
|
||||||
url: `${siteConfig.general.url}/kapcsolat`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ContactLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode
|
|
||||||
}) {
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
@@ -2,11 +2,8 @@ import type { Metadata, Viewport } from "next";
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
|
||||||
import Header from "../../components/Header";
|
|
||||||
import Footer from "../../components/Footer";
|
|
||||||
import { ThemeProvider } from "../../components/ThemeProvider";
|
import { ThemeProvider } from "../../components/ThemeProvider";
|
||||||
import { siteConfig } from "../../config/site";
|
import { siteConfig, getSiteDescription } from "../../config/site";
|
||||||
import { common } from "../../content";
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@@ -18,47 +15,18 @@ const geistMono = Geist_Mono({
|
|||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// WHY hu itt: ez a legkülső, nyelv-független layout (html/body/theme-script
|
||||||
|
// csak egyszer, MITHOME-91/114) — a metadataBase és az alap description a
|
||||||
|
// magyar (alapértelmezett) nyelvet tükrözi, de minden [locale]/[slug]
|
||||||
|
// oldal a saját generateMetadata()-jával felülírja title/description-t
|
||||||
|
// nyelvhelyesen. Ez csak a legelső, JS nélküli betöltéskori fallback.
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
// WHY metadataBase: without it Next resolves relative OG/twitter image URLs
|
// WHY metadataBase: without it Next resolves relative OG/twitter image URLs
|
||||||
// against localhost, producing broken social previews in production.
|
// against localhost, producing broken social previews in production.
|
||||||
metadataBase: new URL(siteConfig.general.url),
|
metadataBase: new URL(siteConfig.general.url),
|
||||||
title: `${siteConfig.general.name} | ${siteConfig.general.description}`,
|
title: siteConfig.general.name,
|
||||||
description: siteConfig.general.description,
|
description: getSiteDescription("hu"),
|
||||||
authors: [{ name: siteConfig.general.name }],
|
authors: [{ name: siteConfig.general.name }],
|
||||||
keywords: ["web hosting", "email szolgáltatás", "DNS adminisztráció", "IT szolgáltatás", "mozdIT"],
|
|
||||||
openGraph: {
|
|
||||||
title: siteConfig.general.name,
|
|
||||||
description: siteConfig.general.description,
|
|
||||||
url: siteConfig.general.url,
|
|
||||||
siteName: siteConfig.general.name,
|
|
||||||
images: [
|
|
||||||
{
|
|
||||||
url: siteConfig.general.ogImage,
|
|
||||||
width: 1200,
|
|
||||||
height: 630,
|
|
||||||
alt: siteConfig.general.name,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
locale: siteConfig.general.locale,
|
|
||||||
type: "website",
|
|
||||||
},
|
|
||||||
twitter: {
|
|
||||||
card: "summary_large_image",
|
|
||||||
title: siteConfig.general.name,
|
|
||||||
description: siteConfig.general.description,
|
|
||||||
images: [siteConfig.general.ogImage],
|
|
||||||
},
|
|
||||||
robots: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
googleBot: {
|
|
||||||
index: true,
|
|
||||||
follow: true,
|
|
||||||
"max-video-preview": -1,
|
|
||||||
"max-image-preview": "large",
|
|
||||||
"max-snippet": -1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Keep Safari's browser chrome neutral; only the in-page staging strip is amber.
|
// Keep Safari's browser chrome neutral; only the in-page staging strip is amber.
|
||||||
@@ -71,7 +39,6 @@ export default function RootLayout({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
const isStaging = process.env.NEXT_PUBLIC_DEPLOY_ENV === 'staging';
|
|
||||||
return (
|
return (
|
||||||
<html lang="hu" suppressHydrationWarning>
|
<html lang="hu" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
@@ -95,16 +62,7 @@ export default function RootLayout({
|
|||||||
style={{ background: 'var(--color-background)', color: 'var(--color-foreground)' }}
|
style={{ background: 'var(--color-background)', color: 'var(--color-foreground)' }}
|
||||||
>
|
>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
{isStaging && (
|
{children}
|
||||||
<div className="bg-amber-400 px-4 py-2 text-center text-xs font-extrabold tracking-[0.18em] text-amber-950 sm:text-sm">
|
|
||||||
{common.staging.banner}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Header />
|
|
||||||
<main className="flex-1">
|
|
||||||
{children}
|
|
||||||
</main>
|
|
||||||
<Footer />
|
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import Link from 'next/link'
|
||||||
|
import { DEFAULT_LOCALE, localePath } from '@/lib/i18n'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MITHOME-91/114: explicit not-found a (frontend) route group szintjén.
|
||||||
|
*
|
||||||
|
* WHY kell ez expliciten: notFound() hívásra (érvénytelen locale vagy slug
|
||||||
|
* a [locale]/[slug] catch-all-ban) Next.js enélkül a beépített, kétértelmű
|
||||||
|
* fallback UI-t próbálja renderelni — ez a (payload) route group saját
|
||||||
|
* <html> gyökerével ütközve ugyanazt a "script tag" / dupla-html hibát
|
||||||
|
* okozta, amit a MITHOME-87-ben már egyszer megoldottunk a (frontend)/
|
||||||
|
* (payload) szétválasztással. Egy saját not-found.tsx a (frontend) alatt
|
||||||
|
* egyértelművé teszi, melyik gyökér html-be kell renderelni.
|
||||||
|
*/
|
||||||
|
export default function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-1 flex-col items-center justify-center py-24 px-4 text-center">
|
||||||
|
<h1 className="text-4xl font-bold mb-4" style={{ color: 'var(--color-foreground)' }}>404</h1>
|
||||||
|
<p className="mb-8" style={{ color: 'var(--color-foreground-muted)' }}>
|
||||||
|
A keresett oldal nem található. / The page you’re looking for could not be found.
|
||||||
|
</p>
|
||||||
|
<Link href={localePath(DEFAULT_LOCALE)} className="btn btn-primary">
|
||||||
|
mozdIT Bt.
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Executable → Regular
+6
-303
@@ -1,305 +1,8 @@
|
|||||||
import { content } from '@/content'
|
import { redirect } from 'next/navigation'
|
||||||
import Image from 'next/image'
|
import { DEFAULT_LOCALE, localePath } from '@/lib/i18n'
|
||||||
|
|
||||||
const { home: pageContent } = content.pages
|
// A puszta domain-gyökér (pl. mozdit.hu/) az alapértelmezett nyelvre
|
||||||
|
// (hu) irányít — a tényleges főoldal a /hu alatt él (MITHOME-91/114).
|
||||||
export default function Home() {
|
export default function RootRedirect() {
|
||||||
return (
|
redirect(localePath(DEFAULT_LOCALE))
|
||||||
<div className="space-y-0">
|
|
||||||
{/* Hero Section */}
|
|
||||||
<section className="bg-gradient-hero py-20 lg:py-28 relative overflow-hidden">
|
|
||||||
{/* Background decoration */}
|
|
||||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
|
||||||
<div
|
|
||||||
className="absolute -top-40 -right-40 w-80 h-80 rounded-full opacity-30 animate-pulse-slow"
|
|
||||||
style={{ background: 'var(--color-primary-200)' }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute -bottom-40 -left-40 w-96 h-96 rounded-full opacity-20 animate-pulse-slow"
|
|
||||||
style={{ background: 'var(--color-accent-200)', animationDelay: '1s' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
|
||||||
<h1
|
|
||||||
className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6 leading-tight animate-fade-in-up"
|
|
||||||
style={{ color: 'var(--color-foreground)' }}
|
|
||||||
>
|
|
||||||
{pageContent.hero.title}
|
|
||||||
</h1>
|
|
||||||
<p
|
|
||||||
className="text-lg md:text-xl max-w-4xl mx-auto mb-8 leading-relaxed animate-fade-in-up"
|
|
||||||
style={{ color: 'var(--color-foreground-muted)', animationDelay: '100ms' }}
|
|
||||||
>
|
|
||||||
{pageContent.hero.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{pageContent.hero.trustBullets && (
|
|
||||||
<div
|
|
||||||
className="flex flex-wrap justify-center gap-4 md:gap-8 mb-10 text-sm font-medium animate-fade-in-up"
|
|
||||||
style={{ color: 'var(--color-foreground-secondary)', animationDelay: '150ms' }}
|
|
||||||
>
|
|
||||||
{pageContent.hero.trustBullets.map((bullet: string, i: number) => (
|
|
||||||
<div key={i} className="flex items-center gap-2">
|
|
||||||
<span style={{ color: 'var(--color-primary-500)' }}>✓</span>
|
|
||||||
{bullet}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className="flex flex-col sm:flex-row gap-4 justify-center items-center animate-fade-in-up"
|
|
||||||
style={{ animationDelay: '200ms' }}
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href={pageContent.hero.cta.primary.href}
|
|
||||||
target={pageContent.hero.cta.primary.external ? '_blank' : undefined}
|
|
||||||
rel={pageContent.hero.cta.primary.external ? 'noopener noreferrer' : undefined}
|
|
||||||
className="btn btn-primary text-lg px-8 py-4 group"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
className="w-5 h-5 transition-transform duration-200 group-hover:scale-110"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10" />
|
|
||||||
</svg>
|
|
||||||
{pageContent.hero.cta.primary.text}
|
|
||||||
</a>
|
|
||||||
{pageContent.hero.cta.secondary && (
|
|
||||||
<a
|
|
||||||
href={pageContent.hero.cta.secondary.href}
|
|
||||||
className="btn btn-secondary text-lg px-8 py-4"
|
|
||||||
>
|
|
||||||
{pageContent.hero.cta.secondary.text}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* USP Section */}
|
|
||||||
<section
|
|
||||||
className="py-20"
|
|
||||||
style={{ background: 'var(--color-background-secondary)' }}
|
|
||||||
>
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center mb-12">
|
|
||||||
<h2
|
|
||||||
className="text-3xl md:text-4xl font-bold mb-4"
|
|
||||||
style={{ color: 'var(--color-foreground)' }}
|
|
||||||
>
|
|
||||||
{pageContent.about.title}
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 stagger-children">
|
|
||||||
{pageContent.about.usps.map((usp, index) => (
|
|
||||||
<div
|
|
||||||
key={usp.id}
|
|
||||||
className="group text-center p-6 rounded-xl transition-all duration-300 hover-lift animate-fade-in-up"
|
|
||||||
style={{
|
|
||||||
background: 'var(--color-background)',
|
|
||||||
border: '1px solid var(--color-border)',
|
|
||||||
animationDelay: `${index * 100}ms`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="icon-container icon-container-lg mx-auto mb-4"
|
|
||||||
>
|
|
||||||
<span className="text-2xl">{usp.icon}</span>
|
|
||||||
</div>
|
|
||||||
<h3
|
|
||||||
className="text-lg font-semibold mb-2 transition-colors duration-200"
|
|
||||||
style={{ color: 'var(--color-foreground)' }}
|
|
||||||
>
|
|
||||||
{usp.title}
|
|
||||||
</h3>
|
|
||||||
<p style={{ color: 'var(--color-foreground-muted)' }}>
|
|
||||||
{usp.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Services Section */}
|
|
||||||
<section
|
|
||||||
className="py-20"
|
|
||||||
style={{ background: 'var(--color-background)' }}
|
|
||||||
>
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center mb-12">
|
|
||||||
<h2
|
|
||||||
className="text-3xl md:text-4xl font-bold mb-4"
|
|
||||||
style={{ color: 'var(--color-foreground)' }}
|
|
||||||
>
|
|
||||||
{pageContent.services.title}
|
|
||||||
</h2>
|
|
||||||
<p
|
|
||||||
className="text-lg max-w-3xl mx-auto"
|
|
||||||
style={{ color: 'var(--color-foreground-muted)' }}
|
|
||||||
>
|
|
||||||
{pageContent.services.subtitle}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
|
||||||
{pageContent.services.items.map((service, index) => (
|
|
||||||
<div
|
|
||||||
key={service.id}
|
|
||||||
className="group card hover-lift"
|
|
||||||
style={{ animationDelay: `${index * 100}ms` }}
|
|
||||||
>
|
|
||||||
<div className="icon-container mb-4">
|
|
||||||
<span className="text-xl">{service.icon}</span>
|
|
||||||
</div>
|
|
||||||
<h3
|
|
||||||
className="text-xl font-semibold mb-3 transition-colors duration-200 group-hover:text-blue-600"
|
|
||||||
style={{ color: 'var(--color-foreground)' }}
|
|
||||||
>
|
|
||||||
{service.title}
|
|
||||||
</h3>
|
|
||||||
<p
|
|
||||||
className="leading-relaxed mb-4"
|
|
||||||
style={{ color: 'var(--color-foreground-muted)' }}
|
|
||||||
>
|
|
||||||
{service.description}
|
|
||||||
</p>
|
|
||||||
<div className="mb-4">
|
|
||||||
<h4
|
|
||||||
className="text-sm font-semibold mb-2"
|
|
||||||
style={{ color: 'var(--color-foreground-secondary)' }}
|
|
||||||
>
|
|
||||||
{pageContent.serviceFeatures.title}
|
|
||||||
</h4>
|
|
||||||
<ul
|
|
||||||
className="text-sm space-y-1.5"
|
|
||||||
style={{ color: 'var(--color-foreground-muted)' }}
|
|
||||||
>
|
|
||||||
{service.features.map((feature, idx) => (
|
|
||||||
<li key={idx} className="flex items-start group/item">
|
|
||||||
<span
|
|
||||||
className="mr-2 transition-transform duration-200 group-hover/item:scale-125"
|
|
||||||
style={{ color: 'var(--color-success-500)' }}
|
|
||||||
>
|
|
||||||
✓
|
|
||||||
</span>
|
|
||||||
{feature}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href="/kapcsolat"
|
|
||||||
className="inline-flex items-center font-medium transition-all duration-200 group/link"
|
|
||||||
style={{ color: 'var(--color-primary-600)' }}
|
|
||||||
>
|
|
||||||
{service.ctaText}
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 ml-1 transition-transform duration-200 group-hover/link:translate-x-1"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Partners Section */}
|
|
||||||
{pageContent.partners.items.length > 0 && (
|
|
||||||
<section className="py-16">
|
|
||||||
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<h2 className="text-3xl font-bold text-center mb-3" style={{ color: 'var(--color-foreground)' }}>
|
|
||||||
{pageContent.partners.title}
|
|
||||||
</h2>
|
|
||||||
<p className="text-center mb-10" style={{ color: 'var(--color-foreground-muted)' }}>
|
|
||||||
{pageContent.partners.subtitle}
|
|
||||||
</p>
|
|
||||||
<div className="flex flex-wrap justify-center items-center gap-10">
|
|
||||||
{pageContent.partners.items.map((partner) => (
|
|
||||||
<a
|
|
||||||
key={partner.name}
|
|
||||||
href={partner.url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="group flex flex-col items-center gap-3 opacity-80 hover:opacity-100 transition-opacity duration-200"
|
|
||||||
title={partner.name}
|
|
||||||
>
|
|
||||||
<span className="relative h-12 w-40">
|
|
||||||
<Image
|
|
||||||
src={partner.logo}
|
|
||||||
alt={partner.name}
|
|
||||||
fill
|
|
||||||
sizes="160px"
|
|
||||||
className="object-contain"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
<span className="text-sm" style={{ color: 'var(--color-foreground-muted)' }}>
|
|
||||||
{partner.name}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* CTA Section */}
|
|
||||||
<section
|
|
||||||
className="py-20 relative overflow-hidden"
|
|
||||||
style={{ background: 'var(--color-foreground)' }}
|
|
||||||
>
|
|
||||||
{/* Animated background elements */}
|
|
||||||
<div className="absolute inset-0 overflow-hidden pointer-events-none opacity-10">
|
|
||||||
<div
|
|
||||||
className="absolute top-10 left-10 w-32 h-32 rounded-full animate-float"
|
|
||||||
style={{ background: 'var(--color-primary-500)' }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute bottom-10 right-20 w-24 h-24 rounded-full animate-float"
|
|
||||||
style={{ background: 'var(--color-accent-500)', animationDelay: '0.5s' }}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="absolute top-1/2 right-1/4 w-16 h-16 rounded-full animate-float"
|
|
||||||
style={{ background: 'var(--color-primary-400)', animationDelay: '1s' }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
|
||||||
<h2
|
|
||||||
className="text-3xl md:text-4xl font-bold mb-4"
|
|
||||||
style={{ color: 'var(--color-background)' }}
|
|
||||||
>
|
|
||||||
{pageContent.cta.title}
|
|
||||||
</h2>
|
|
||||||
<p
|
|
||||||
className="text-xl mb-8 max-w-2xl mx-auto"
|
|
||||||
style={{ color: 'var(--color-background)', opacity: 0.8 }}
|
|
||||||
>
|
|
||||||
{pageContent.cta.subtitle}
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
href="/kapcsolat"
|
|
||||||
className="btn btn-primary text-lg px-8 py-4 hover-glow"
|
|
||||||
>
|
|
||||||
{pageContent.cta.button}
|
|
||||||
<svg
|
|
||||||
className="w-5 h-5 ml-2"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,44 @@
|
|||||||
import { render, screen } from '@testing-library/react'
|
import { render, screen } from '@testing-library/react'
|
||||||
import '@testing-library/jest-dom'
|
import '@testing-library/jest-dom'
|
||||||
import Footer from './Footer'
|
import Footer from './Footer'
|
||||||
import { common } from '@/content'
|
import { common, content } from '@/content'
|
||||||
|
import { getFooterNavigation, getFooterLegalLinks, getSiteDescription } from '@/config/site'
|
||||||
|
|
||||||
|
// MITHOME-91/114: Footer lett props-alapú (locale-aware tartalom a szülő
|
||||||
|
// [locale] layoutból jön, Payload Local API-n keresztül) — a teszt a
|
||||||
|
// content/*.json fixture-öket + a valódi config/site.ts helper-eket adja
|
||||||
|
// át, hogy a viselkedés a ténylegeshez hasonló maradjon.
|
||||||
|
const footerProps = {
|
||||||
|
nav: getFooterNavigation('hu'),
|
||||||
|
legalLinks: getFooterLegalLinks('hu'),
|
||||||
|
homeHref: '/hu',
|
||||||
|
description: getSiteDescription('hu'),
|
||||||
|
contactEmail: 'info@mozdit.hu',
|
||||||
|
footerAddress: common.footer.address,
|
||||||
|
footerCopyright: common.footer.copyright,
|
||||||
|
homeServices: content.pages.home.services.items.map((item) => ({ id: item.id, title: item.title, icon: item.icon })),
|
||||||
|
locale: 'hu' as const,
|
||||||
|
}
|
||||||
|
|
||||||
describe('Footer', () => {
|
describe('Footer', () => {
|
||||||
it('should render company information', () => {
|
it('should render company information', () => {
|
||||||
render(<Footer />)
|
render(<Footer {...footerProps} />)
|
||||||
|
|
||||||
expect(screen.getByText('mozdIT Bt.')).toBeInTheDocument()
|
expect(screen.getByText('mozdIT Bt.')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Megbízható web- és email szolgáltatás személyre szabott támogatással. Stabil tárhely, üzembiztos levelezés és DNS adminisztráció gyors reakcióval.')).toBeInTheDocument()
|
expect(screen.getByText('Megbízható web- és email szolgáltatás személyre szabott támogatással. Stabil tárhely, üzembiztos levelezés és DNS adminisztráció gyors reakcióval.')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render email and company details', () => {
|
it('should render email and company details', () => {
|
||||||
render(<Footer />)
|
render(<Footer {...footerProps} />)
|
||||||
|
|
||||||
expect(screen.getByText('info@mozdit.hu')).toBeInTheDocument()
|
expect(screen.getByText('info@mozdit.hu')).toBeInTheDocument()
|
||||||
expect(screen.getAllByText('mozdIT Bt.').length).toBeGreaterThan(0)
|
expect(screen.getAllByText('mozdIT Bt.').length).toBeGreaterThan(0)
|
||||||
// Address is CMS-editable (common.json footer.address) — assert the source value
|
// Address is CMS-editable (Payload Common global footer.address) — assert the source value
|
||||||
expect(screen.getByText(common.footer.address)).toBeInTheDocument()
|
expect(screen.getByText(common.footer.address)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render navigation links', () => {
|
it('should render navigation links', () => {
|
||||||
render(<Footer />)
|
render(<Footer {...footerProps} />)
|
||||||
|
|
||||||
expect(screen.getByText('Kezdőlap')).toBeInTheDocument()
|
expect(screen.getByText('Kezdőlap')).toBeInTheDocument()
|
||||||
expect(screen.getByText('Rólunk')).toBeInTheDocument()
|
expect(screen.getByText('Rólunk')).toBeInTheDocument()
|
||||||
@@ -30,7 +47,7 @@ describe('Footer', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render service sections', () => {
|
it('should render service sections', () => {
|
||||||
render(<Footer />)
|
render(<Footer {...footerProps} />)
|
||||||
|
|
||||||
expect(screen.getByText('Webtárhely (Hosting)')).toBeInTheDocument()
|
expect(screen.getByText('Webtárhely (Hosting)')).toBeInTheDocument()
|
||||||
expect(screen.getByText('E-mail szolgáltatás')).toBeInTheDocument()
|
expect(screen.getByText('E-mail szolgáltatás')).toBeInTheDocument()
|
||||||
@@ -39,21 +56,21 @@ describe('Footer', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render copyright notice from common.json with the current year', () => {
|
it('should render copyright notice from common.json with the current year', () => {
|
||||||
render(<Footer />)
|
render(<Footer {...footerProps} />)
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear()
|
const currentYear = new Date().getFullYear()
|
||||||
expect(screen.getByText(`© 2002–${currentYear} mozdIT Bt. Minden jog fenntartva.`)).toBeInTheDocument()
|
expect(screen.getByText(`© 2002–${currentYear} mozdIT Bt. Minden jog fenntartva.`)).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render legal links', () => {
|
it('should render legal links', () => {
|
||||||
render(<Footer />)
|
render(<Footer {...footerProps} />)
|
||||||
|
|
||||||
expect(screen.getAllByText('Adatvédelmi tájékoztató')).toHaveLength(2) // Appears in both sections
|
expect(screen.getAllByText('Adatvédelmi tájékoztató')).toHaveLength(2) // Appears in both sections
|
||||||
expect(screen.getAllByText('Használati feltételek')).toHaveLength(2) // Appears in both sections
|
expect(screen.getAllByText('Használati feltételek')).toHaveLength(2) // Appears in both sections
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render with proper grid layout', () => {
|
it('should render with proper grid layout', () => {
|
||||||
const { container } = render(<Footer />)
|
const { container } = render(<Footer {...footerProps} />)
|
||||||
|
|
||||||
const gridContainer = container.querySelector('.grid.grid-cols-1.md\\:grid-cols-4')
|
const gridContainer = container.querySelector('.grid.grid-cols-1.md\\:grid-cols-4')
|
||||||
expect(gridContainer).toBeInTheDocument()
|
expect(gridContainer).toBeInTheDocument()
|
||||||
@@ -63,7 +80,7 @@ describe('Footer', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render with proper semantic structure', () => {
|
it('should render with proper semantic structure', () => {
|
||||||
const { container } = render(<Footer />)
|
const { container } = render(<Footer {...footerProps} />)
|
||||||
|
|
||||||
// Should have a footer element
|
// Should have a footer element
|
||||||
const footer = container.firstChild as HTMLElement
|
const footer = container.firstChild as HTMLElement
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { siteConfig } from '@/config/site'
|
import { siteConfig } from '@/config/site'
|
||||||
import { content } from '@/content'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import type { NavigationItem } from '@/types/site'
|
||||||
|
import type { Locale } from '@/lib/i18n'
|
||||||
|
|
||||||
export default function Footer() {
|
type FooterServiceItem = { id: string; title: string; icon: string }
|
||||||
|
|
||||||
|
type FooterProps = {
|
||||||
|
nav: NavigationItem[]
|
||||||
|
legalLinks: NavigationItem[]
|
||||||
|
homeHref: string
|
||||||
|
description: string
|
||||||
|
contactEmail: string
|
||||||
|
footerAddress: string
|
||||||
|
footerCopyright: string
|
||||||
|
homeServices: FooterServiceItem[]
|
||||||
|
locale: Locale
|
||||||
|
}
|
||||||
|
|
||||||
|
const SECTION_LABELS: Record<Locale, { navigation: string; services: string; support: string }> = {
|
||||||
|
hu: { navigation: 'Navigáció', services: 'Szolgáltatások', support: 'Műszaki támogatás' },
|
||||||
|
en: { navigation: 'Navigation', services: 'Services', support: 'Technical support' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Footer({ nav, legalLinks, homeHref, description, contactEmail, footerAddress, footerCopyright, homeServices, locale }: FooterProps) {
|
||||||
|
const t = SECTION_LABELS[locale]
|
||||||
return (
|
return (
|
||||||
<footer
|
<footer
|
||||||
className="border-t"
|
className="border-t"
|
||||||
@@ -18,7 +39,7 @@ export default function Footer() {
|
|||||||
{/* Company Info */}
|
{/* Company Info */}
|
||||||
<div className="md:col-span-2">
|
<div className="md:col-span-2">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href={homeHref}
|
||||||
className="inline-flex items-center gap-2 text-xl font-bold mb-4 transition-colors duration-200"
|
className="inline-flex items-center gap-2 text-xl font-bold mb-4 transition-colors duration-200"
|
||||||
style={{ color: 'var(--color-primary-600)' }}
|
style={{ color: 'var(--color-primary-600)' }}
|
||||||
>
|
>
|
||||||
@@ -32,11 +53,11 @@ export default function Footer() {
|
|||||||
className="mb-6 max-w-md leading-relaxed"
|
className="mb-6 max-w-md leading-relaxed"
|
||||||
style={{ color: 'var(--color-foreground-muted)' }}
|
style={{ color: 'var(--color-foreground-muted)' }}
|
||||||
>
|
>
|
||||||
{siteConfig.general.description}
|
{description}
|
||||||
</p>
|
</p>
|
||||||
<div className="space-y-2 text-sm" style={{ color: 'var(--color-foreground-muted)' }}>
|
<div className="space-y-2 text-sm" style={{ color: 'var(--color-foreground-muted)' }}>
|
||||||
<a
|
<a
|
||||||
href={`mailto:${siteConfig.contact.email}`}
|
href={`mailto:${contactEmail}`}
|
||||||
className="flex items-center gap-2 group transition-colors duration-200 hover:text-blue-600"
|
className="flex items-center gap-2 group transition-colors duration-200 hover:text-blue-600"
|
||||||
style={{ color: 'var(--color-foreground-secondary)' }}
|
style={{ color: 'var(--color-foreground-secondary)' }}
|
||||||
>
|
>
|
||||||
@@ -46,7 +67,7 @@ export default function Footer() {
|
|||||||
>
|
>
|
||||||
✉️
|
✉️
|
||||||
</span>
|
</span>
|
||||||
<span>{siteConfig.contact.email}</span>
|
<span>{contactEmail}</span>
|
||||||
</a>
|
</a>
|
||||||
<div
|
<div
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
@@ -58,7 +79,7 @@ export default function Footer() {
|
|||||||
>
|
>
|
||||||
🏢
|
🏢
|
||||||
</span>
|
</span>
|
||||||
<span>{content.common.footer.address}</span>
|
<span>{footerAddress}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -69,10 +90,10 @@ export default function Footer() {
|
|||||||
className="text-sm font-semibold uppercase tracking-wider mb-4"
|
className="text-sm font-semibold uppercase tracking-wider mb-4"
|
||||||
style={{ color: 'var(--color-foreground)' }}
|
style={{ color: 'var(--color-foreground)' }}
|
||||||
>
|
>
|
||||||
Navigáció
|
{t.navigation}
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{siteConfig.navigation.footer.map((item) => (
|
{nav.map((item) => (
|
||||||
<li key={item.href}>
|
<li key={item.href}>
|
||||||
<a
|
<a
|
||||||
href={item.href}
|
href={item.href}
|
||||||
@@ -102,10 +123,10 @@ export default function Footer() {
|
|||||||
className="text-sm font-semibold uppercase tracking-wider mb-4"
|
className="text-sm font-semibold uppercase tracking-wider mb-4"
|
||||||
style={{ color: 'var(--color-foreground)' }}
|
style={{ color: 'var(--color-foreground)' }}
|
||||||
>
|
>
|
||||||
Szolgáltatások
|
{t.services}
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="space-y-3">
|
<ul className="space-y-3">
|
||||||
{content.pages.home.services.items.map((service) => (
|
{homeServices.map((service) => (
|
||||||
<li
|
<li
|
||||||
key={service.id}
|
key={service.id}
|
||||||
className="flex items-center gap-2 text-sm"
|
className="flex items-center gap-2 text-sm"
|
||||||
@@ -120,7 +141,7 @@ export default function Footer() {
|
|||||||
style={{ color: 'var(--color-foreground-muted)' }}
|
style={{ color: 'var(--color-foreground-muted)' }}
|
||||||
>
|
>
|
||||||
<span className="text-base">🛠️</span>
|
<span className="text-base">🛠️</span>
|
||||||
Műszaki támogatás
|
{t.support}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,11 +157,11 @@ export default function Footer() {
|
|||||||
className="text-sm"
|
className="text-sm"
|
||||||
style={{ color: 'var(--color-foreground-muted)' }}
|
style={{ color: 'var(--color-foreground-muted)' }}
|
||||||
>
|
>
|
||||||
{/* Copyright text is CMS-editable (common.json); {year} resolves to the current year */}
|
{/* Copyright text is CMS-editable (Payload Common global); {year} resolves to the current year */}
|
||||||
{content.common.footer.copyright.replace('{year}', String(new Date().getFullYear()))}
|
{footerCopyright.replace('{year}', String(new Date().getFullYear()))}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
{siteConfig.footer.links.map((link) => (
|
{legalLinks.map((link) => (
|
||||||
<a
|
<a
|
||||||
key={link.href}
|
key={link.href}
|
||||||
href={link.href}
|
href={link.href}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import '@testing-library/jest-dom'
|
|||||||
import userEvent from '@testing-library/user-event'
|
import userEvent from '@testing-library/user-event'
|
||||||
import Header from './Header'
|
import Header from './Header'
|
||||||
import { common } from '@/content'
|
import { common } from '@/content'
|
||||||
|
import { getMainNavigation } from '@/config/site'
|
||||||
|
|
||||||
// Mock Next.js Link component
|
// Mock Next.js Link component
|
||||||
jest.mock('next/link', () => {
|
jest.mock('next/link', () => {
|
||||||
@@ -11,14 +12,23 @@ jest.mock('next/link', () => {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// MITHOME-91/114: Header lett props-alapú (locale-aware nav/a11y a szülő
|
||||||
|
// [locale] layoutból jön) — a teszt a valódi getMainNavigation('hu')-t adja
|
||||||
|
// át, hogy a feliratok/hrefek a tényleges alkalmazás-viselkedést tükrözzék.
|
||||||
|
const headerProps = {
|
||||||
|
nav: getMainNavigation('hu'),
|
||||||
|
homeHref: '/hu',
|
||||||
|
a11y: common.a11y,
|
||||||
|
}
|
||||||
|
|
||||||
describe('Header', () => {
|
describe('Header', () => {
|
||||||
it('should render the company logo', () => {
|
it('should render the company logo', () => {
|
||||||
render(<Header />)
|
render(<Header {...headerProps} />)
|
||||||
expect(screen.getByAltText('mozdIT Bt.')).toBeInTheDocument()
|
expect(screen.getByAltText('mozdIT Bt.')).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render all navigation links in desktop menu', () => {
|
it('should render all navigation links in desktop menu', () => {
|
||||||
render(<Header />)
|
render(<Header {...headerProps} />)
|
||||||
|
|
||||||
// Desktop menu should contain all links with specific structures
|
// Desktop menu should contain all links with specific structures
|
||||||
const desktopMenu = document.querySelector('.hidden.md\\:flex')
|
const desktopMenu = document.querySelector('.hidden.md\\:flex')
|
||||||
@@ -32,7 +42,7 @@ describe('Header', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render contact button with correct styling', () => {
|
it('should render contact button with correct styling', () => {
|
||||||
render(<Header />)
|
render(<Header {...headerProps} />)
|
||||||
|
|
||||||
const contactButtons = screen.getAllByText('Kapcsolat')
|
const contactButtons = screen.getAllByText('Kapcsolat')
|
||||||
expect(contactButtons.length).toBeGreaterThan(0)
|
expect(contactButtons.length).toBeGreaterThan(0)
|
||||||
@@ -49,7 +59,7 @@ describe('Header', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should render hamburger menu button on mobile', () => {
|
it('should render hamburger menu button on mobile', () => {
|
||||||
render(<Header />)
|
render(<Header {...headerProps} />)
|
||||||
|
|
||||||
// The hamburger menu button is hidden by default in desktop view
|
// The hamburger menu button is hidden by default in desktop view
|
||||||
// We can test its presence even if not visible
|
// We can test its presence even if not visible
|
||||||
@@ -58,14 +68,14 @@ describe('Header', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should have proper accessibility attributes', () => {
|
it('should have proper accessibility attributes', () => {
|
||||||
render(<Header />)
|
render(<Header {...headerProps} />)
|
||||||
|
|
||||||
const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') })
|
const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') })
|
||||||
expect(hamburgerButton).toHaveAttribute('aria-expanded', 'false')
|
expect(hamburgerButton).toHaveAttribute('aria-expanded', 'false')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should render with proper semantic structure', () => {
|
it('should render with proper semantic structure', () => {
|
||||||
const { container } = render(<Header />)
|
const { container } = render(<Header {...headerProps} />)
|
||||||
|
|
||||||
// Should have header element with proper structure
|
// Should have header element with proper structure
|
||||||
const header = container.firstChild as HTMLElement
|
const header = container.firstChild as HTMLElement
|
||||||
@@ -81,7 +91,7 @@ describe('Header', () => {
|
|||||||
|
|
||||||
it('should toggle mobile menu when hamburger button is clicked', async () => {
|
it('should toggle mobile menu when hamburger button is clicked', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
render(<Header />)
|
render(<Header {...headerProps} />)
|
||||||
|
|
||||||
const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') })
|
const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') })
|
||||||
|
|
||||||
@@ -99,7 +109,7 @@ describe('Header', () => {
|
|||||||
|
|
||||||
it('should close mobile menu when navigation link is clicked', async () => {
|
it('should close mobile menu when navigation link is clicked', async () => {
|
||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
render(<Header />)
|
render(<Header {...headerProps} />)
|
||||||
|
|
||||||
const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') })
|
const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') })
|
||||||
|
|
||||||
@@ -120,31 +130,31 @@ describe('Header', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should have correct navigation links with proper hrefs', () => {
|
it('should have correct navigation links with proper hrefs', () => {
|
||||||
render(<Header />)
|
render(<Header {...headerProps} />)
|
||||||
|
|
||||||
// Check for home link
|
// Check for home link
|
||||||
const homeLinks = screen.getAllByText('Kezdőlap')
|
const homeLinks = screen.getAllByText('Kezdőlap')
|
||||||
expect(homeLinks.length).toBeGreaterThan(0)
|
expect(homeLinks.length).toBeGreaterThan(0)
|
||||||
expect(homeLinks[0].closest('a')).toHaveAttribute('href', '/')
|
expect(homeLinks[0].closest('a')).toHaveAttribute('href', '/hu')
|
||||||
|
|
||||||
// Check for about link
|
// Check for about link
|
||||||
const aboutLinks = screen.getAllByText('Rólunk')
|
const aboutLinks = screen.getAllByText('Rólunk')
|
||||||
expect(aboutLinks.length).toBeGreaterThan(0)
|
expect(aboutLinks.length).toBeGreaterThan(0)
|
||||||
expect(aboutLinks[0].closest('a')).toHaveAttribute('href', '/rolunk')
|
expect(aboutLinks[0].closest('a')).toHaveAttribute('href', '/hu/rolunk')
|
||||||
|
|
||||||
// Check for services link
|
// Check for services link
|
||||||
const servicesLinks = screen.getAllByText('Szolgáltatások')
|
const servicesLinks = screen.getAllByText('Szolgáltatások')
|
||||||
expect(servicesLinks.length).toBeGreaterThan(0)
|
expect(servicesLinks.length).toBeGreaterThan(0)
|
||||||
expect(servicesLinks[0].closest('a')).toHaveAttribute('href', '/szolgaltatasok')
|
expect(servicesLinks[0].closest('a')).toHaveAttribute('href', '/hu/szolgaltatasok')
|
||||||
|
|
||||||
// Check for contact link
|
// Check for contact link
|
||||||
const contactLinks = screen.getAllByText('Kapcsolat')
|
const contactLinks = screen.getAllByText('Kapcsolat')
|
||||||
expect(contactLinks.length).toBeGreaterThan(0)
|
expect(contactLinks.length).toBeGreaterThan(0)
|
||||||
expect(contactLinks[0].closest('a')).toHaveAttribute('href', '/kapcsolat')
|
expect(contactLinks[0].closest('a')).toHaveAttribute('href', '/hu/kapcsolat')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should have proper responsive classes', () => {
|
it('should have proper responsive classes', () => {
|
||||||
const { container } = render(<Header />)
|
const { container } = render(<Header {...headerProps} />)
|
||||||
|
|
||||||
// Desktop menu should be hidden on mobile
|
// Desktop menu should be hidden on mobile
|
||||||
const desktopMenu = container.querySelector('.hidden.md\\:flex')
|
const desktopMenu = container.querySelector('.hidden.md\\:flex')
|
||||||
|
|||||||
@@ -1,13 +1,19 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { siteConfig } from '@/config/site'
|
import { siteConfig } from '@/config/site'
|
||||||
import { common } from '@/content'
|
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { ThemeToggle } from './ThemeProvider'
|
import { ThemeToggle } from './ThemeProvider'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
|
import type { NavigationItem } from '@/types/site'
|
||||||
|
|
||||||
export default function Header() {
|
type HeaderProps = {
|
||||||
|
nav: NavigationItem[]
|
||||||
|
homeHref: string
|
||||||
|
a11y: { openMenu: string; closeMenu: string }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Header({ nav, homeHref, a11y }: HeaderProps) {
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||||
const [isScrolled, setIsScrolled] = useState(false)
|
const [isScrolled, setIsScrolled] = useState(false)
|
||||||
|
|
||||||
@@ -49,7 +55,7 @@ export default function Header() {
|
|||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<Link
|
<Link
|
||||||
href="/"
|
href={homeHref}
|
||||||
className="group flex items-center gap-2 transition-all duration-200"
|
className="group flex items-center gap-2 transition-all duration-200"
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
@@ -64,7 +70,7 @@ export default function Header() {
|
|||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
<div className="hidden md:flex items-center space-x-1">
|
<div className="hidden md:flex items-center space-x-1">
|
||||||
{siteConfig.navigation.main.map((item) => (
|
{nav.map((item) => (
|
||||||
<a
|
<a
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
@@ -115,7 +121,7 @@ export default function Header() {
|
|||||||
}}
|
}}
|
||||||
aria-expanded={isMenuOpen}
|
aria-expanded={isMenuOpen}
|
||||||
>
|
>
|
||||||
<span className="sr-only">{isMenuOpen ? common.a11y.closeMenu : common.a11y.openMenu}</span>
|
<span className="sr-only">{isMenuOpen ? a11y.closeMenu : a11y.openMenu}</span>
|
||||||
<div className="relative w-6 h-6">
|
<div className="relative w-6 h-6">
|
||||||
{/* Hamburger to X animation */}
|
{/* Hamburger to X animation */}
|
||||||
<span
|
<span
|
||||||
@@ -151,7 +157,7 @@ export default function Header() {
|
|||||||
className="py-3 space-y-1 border-t"
|
className="py-3 space-y-1 border-t"
|
||||||
style={{ borderColor: 'var(--color-border)' }}
|
style={{ borderColor: 'var(--color-border)' }}
|
||||||
>
|
>
|
||||||
{siteConfig.navigation.main.map((item, index) => (
|
{nav.map((item, index) => (
|
||||||
<a
|
<a
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
|
|||||||
+9
-16
@@ -1,20 +1,13 @@
|
|||||||
import { siteConfig } from '@/config/site'
|
import { localePath, type Locale } from '@/lib/i18n'
|
||||||
import { content } from '@/content'
|
import type { getAboutContent } from '@/lib/payload-content'
|
||||||
import type { Metadata } from 'next'
|
|
||||||
|
|
||||||
const { about: pageContent } = content.pages
|
type AboutViewProps = {
|
||||||
|
content: Awaited<ReturnType<typeof getAboutContent>>
|
||||||
export const metadata: Metadata = {
|
locale: Locale
|
||||||
title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
|
|
||||||
description: pageContent.meta.description,
|
|
||||||
openGraph: {
|
|
||||||
title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
|
|
||||||
description: pageContent.meta.ogDescription,
|
|
||||||
url: `${siteConfig.general.url}/rolunk`,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AboutPage() {
|
export default function AboutView({ content: pageContent, locale }: AboutViewProps) {
|
||||||
|
const contactHref = localePath(locale, 'contact')
|
||||||
return (
|
return (
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
@@ -92,7 +85,7 @@ export default function AboutPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 stagger-children">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 stagger-children">
|
||||||
{pageContent.mission.values.map((item, index) => (
|
{(pageContent.mission.values ?? []).map((item, index) => (
|
||||||
<div
|
<div
|
||||||
key={item.title}
|
key={item.title}
|
||||||
className="group text-center p-8 rounded-xl hover-lift animate-fade-in-up"
|
className="group text-center p-8 rounded-xl hover-lift animate-fade-in-up"
|
||||||
@@ -182,7 +175,7 @@ export default function AboutPage() {
|
|||||||
{pageContent.cta.subtitle}
|
{pageContent.cta.subtitle}
|
||||||
</p>
|
</p>
|
||||||
<a
|
<a
|
||||||
href="/kapcsolat"
|
href={contactHref}
|
||||||
className="btn btn-primary text-lg px-8 py-4 hover-glow"
|
className="btn btn-primary text-lg px-8 py-4 hover-glow"
|
||||||
>
|
>
|
||||||
{pageContent.cta.button}
|
{pageContent.cta.button}
|
||||||
+28
-11
@@ -1,12 +1,25 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { siteConfig } from '@/config/site'
|
|
||||||
import { content } from '@/content'
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { siteConfig } from '@/config/site'
|
||||||
|
import { localePath, type Locale } from '@/lib/i18n'
|
||||||
|
import type { getContactContent } from '@/lib/payload-content'
|
||||||
|
|
||||||
const { contact: pageContent } = content.pages
|
type ContactViewProps = {
|
||||||
|
content: Awaited<ReturnType<typeof getContactContent>>
|
||||||
|
contactEmail: string
|
||||||
|
footerAddress: string
|
||||||
|
webmailHref: string
|
||||||
|
locale: Locale
|
||||||
|
}
|
||||||
|
|
||||||
export default function ContactPage() {
|
export default function ContactView({ content: pageContent, contactEmail, footerAddress, webmailHref, locale }: ContactViewProps) {
|
||||||
|
// WHY placeholder-csere: a GDPR-szöveg (Payload Contact.form.fields.gdpr.label)
|
||||||
|
// egy {privacyHref} tokent tartalmaz a beágyazott <a> linkben, mert a
|
||||||
|
// tényleges adatvédelmi oldal útvonala nyelvenként eltér (MITHOME-114) — a
|
||||||
|
// korábbi, hardcode-olt "/adatkezelesi-tajekoztato" út sosem egyezett a
|
||||||
|
// valódi oldallal, ez javítja azt is.
|
||||||
|
const gdprLabel = pageContent.form.fields.gdpr.label.replace('{privacyHref}', localePath(locale, 'privacy'))
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
email: '',
|
email: '',
|
||||||
@@ -135,7 +148,7 @@ export default function ContactPage() {
|
|||||||
{submitStatus === 'error' && (
|
{submitStatus === 'error' && (
|
||||||
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-md">
|
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-md">
|
||||||
<p className="text-red-800">
|
<p className="text-red-800">
|
||||||
❌ {pageContent.form.errorMessage.replace('{email}', siteConfig.contact.email)}
|
❌ {pageContent.form.errorMessage.replace('{email}', contactEmail)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -224,7 +237,7 @@ export default function ContactPage() {
|
|||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className="text-sm text-gray-700"
|
className="text-sm text-gray-700"
|
||||||
dangerouslySetInnerHTML={{ __html: pageContent.form.fields.gdpr.label + ' *' }}
|
dangerouslySetInnerHTML={{ __html: gdprLabel + ' *' }}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{errors.gdprConsent && <p className="mt-1 text-sm text-red-600">{errors.gdprConsent}</p>}
|
{errors.gdprConsent && <p className="mt-1 text-sm text-red-600">{errors.gdprConsent}</p>}
|
||||||
@@ -256,10 +269,10 @@ export default function ContactPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-gray-900 mb-1">{pageContent.info.email.title}</h3>
|
<h3 className="font-semibold text-gray-900 mb-1">{pageContent.info.email.title}</h3>
|
||||||
<a
|
<a
|
||||||
href={`mailto:${siteConfig.contact.email}`}
|
href={`mailto:${contactEmail}`}
|
||||||
className="text-blue-600 hover:text-blue-700"
|
className="text-blue-600 hover:text-blue-700"
|
||||||
>
|
>
|
||||||
{siteConfig.contact.email}
|
{contactEmail}
|
||||||
</a>
|
</a>
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
{pageContent.info.email.responseTime}
|
{pageContent.info.email.responseTime}
|
||||||
@@ -274,7 +287,7 @@ export default function ContactPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-gray-900 mb-1">{pageContent.info.company.title}</h3>
|
<h3 className="font-semibold text-gray-900 mb-1">{pageContent.info.company.title}</h3>
|
||||||
<p className="text-gray-700">{siteConfig.general.name}</p>
|
<p className="text-gray-700">{siteConfig.general.name}</p>
|
||||||
<p className="text-sm text-gray-600">{content.common.footer.address}</p>
|
<p className="text-sm text-gray-600">{footerAddress}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -285,7 +298,11 @@ export default function ContactPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-gray-900 mb-1">{pageContent.info.webmail.title}</h3>
|
<h3 className="font-semibold text-gray-900 mb-1">{pageContent.info.webmail.title}</h3>
|
||||||
<a
|
<a
|
||||||
href={content.pages.home.hero.cta.primary.href}
|
// WHY webmailHref: a migráció előtti kód itt is a
|
||||||
|
// "/kapcsolat" hrefet használta (ugyanaz a bug, mint a
|
||||||
|
// Szolgáltatások oldalon) — javítva a tényleges webmail
|
||||||
|
// URL-re.
|
||||||
|
href={webmailHref}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-blue-600 hover:text-blue-700"
|
className="text-blue-600 hover:text-blue-700"
|
||||||
@@ -307,7 +324,7 @@ export default function ContactPage() {
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{pageContent.faq.items.map((item, index) => (
|
{(pageContent.faq.items ?? []).map((item, index) => (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
<h3 className="font-semibold text-gray-900 mb-2">{item.question}</h3>
|
<h3 className="font-semibold text-gray-900 mb-2">{item.question}</h3>
|
||||||
<p className="text-gray-600 text-sm">{item.answer}</p>
|
<p className="text-gray-600 text-sm">{item.answer}</p>
|
||||||
Executable
+321
@@ -0,0 +1,321 @@
|
|||||||
|
import Image from 'next/image'
|
||||||
|
import { localePath, type Locale } from '@/lib/i18n'
|
||||||
|
import type { getHomeContent, PartnerView } from '@/lib/payload-content'
|
||||||
|
|
||||||
|
type HomeViewProps = {
|
||||||
|
content: Awaited<ReturnType<typeof getHomeContent>>
|
||||||
|
partners: PartnerView[]
|
||||||
|
locale: Locale
|
||||||
|
}
|
||||||
|
|
||||||
|
// WHY hardcoded itt: a Partners collection (MITHOME-89) csak name/url/logo-t
|
||||||
|
// tárol, a szekció saját címe/alcíme sosem volt Payload-tartalom (a Home
|
||||||
|
// Globalból is szándékosan kimaradt, lásd src/globals/Home.ts) — ugyanaz a
|
||||||
|
// minta, mint a config/site.ts navigáció-feliratoknál.
|
||||||
|
const PARTNERS_COPY: Record<Locale, { title: string; subtitle: string }> = {
|
||||||
|
hu: { title: 'Partnereink', subtitle: 'Akikkel együtt dolgozunk' },
|
||||||
|
en: { title: 'Our Partners', subtitle: 'Who we work with' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HomeView({ content: pageContent, partners, locale }: HomeViewProps) {
|
||||||
|
const contactHref = localePath(locale, 'contact')
|
||||||
|
const partnersCopy = PARTNERS_COPY[locale]
|
||||||
|
return (
|
||||||
|
<div className="space-y-0">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="bg-gradient-hero py-20 lg:py-28 relative overflow-hidden">
|
||||||
|
{/* Background decoration */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||||
|
<div
|
||||||
|
className="absolute -top-40 -right-40 w-80 h-80 rounded-full opacity-30 animate-pulse-slow"
|
||||||
|
style={{ background: 'var(--color-primary-200)' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute -bottom-40 -left-40 w-96 h-96 rounded-full opacity-20 animate-pulse-slow"
|
||||||
|
style={{ background: 'var(--color-accent-200)', animationDelay: '1s' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||||
|
<h1
|
||||||
|
className="text-4xl md:text-5xl lg:text-6xl font-bold mb-6 leading-tight animate-fade-in-up"
|
||||||
|
style={{ color: 'var(--color-foreground)' }}
|
||||||
|
>
|
||||||
|
{pageContent.hero.title}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
className="text-lg md:text-xl max-w-4xl mx-auto mb-8 leading-relaxed animate-fade-in-up"
|
||||||
|
style={{ color: 'var(--color-foreground-muted)', animationDelay: '100ms' }}
|
||||||
|
>
|
||||||
|
{pageContent.hero.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{pageContent.hero.trustBullets && (
|
||||||
|
<div
|
||||||
|
className="flex flex-wrap justify-center gap-4 md:gap-8 mb-10 text-sm font-medium animate-fade-in-up"
|
||||||
|
style={{ color: 'var(--color-foreground-secondary)', animationDelay: '150ms' }}
|
||||||
|
>
|
||||||
|
{pageContent.hero.trustBullets.map((bullet: string, i: number) => (
|
||||||
|
<div key={i} className="flex items-center gap-2">
|
||||||
|
<span style={{ color: 'var(--color-primary-500)' }}>✓</span>
|
||||||
|
{bullet}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="flex flex-col sm:flex-row gap-4 justify-center items-center animate-fade-in-up"
|
||||||
|
style={{ animationDelay: '200ms' }}
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href={pageContent.hero.cta.primary.href}
|
||||||
|
target={pageContent.hero.cta.primary.external ? '_blank' : undefined}
|
||||||
|
rel={pageContent.hero.cta.primary.external ? 'noopener noreferrer' : undefined}
|
||||||
|
className="btn btn-primary text-lg px-8 py-4 group"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 transition-transform duration-200 group-hover:scale-110"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10" />
|
||||||
|
</svg>
|
||||||
|
{pageContent.hero.cta.primary.text}
|
||||||
|
</a>
|
||||||
|
{pageContent.hero.cta.secondary && (
|
||||||
|
<a
|
||||||
|
href={pageContent.hero.cta.secondary.href}
|
||||||
|
className="btn btn-secondary text-lg px-8 py-4"
|
||||||
|
>
|
||||||
|
{pageContent.hero.cta.secondary.text}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* USP Section */}
|
||||||
|
<section
|
||||||
|
className="py-20"
|
||||||
|
style={{ background: 'var(--color-background-secondary)' }}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h2
|
||||||
|
className="text-3xl md:text-4xl font-bold mb-4"
|
||||||
|
style={{ color: 'var(--color-foreground)' }}
|
||||||
|
>
|
||||||
|
{pageContent.about.title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 stagger-children">
|
||||||
|
{(pageContent.about.usps ?? []).map((usp, index) => (
|
||||||
|
<div
|
||||||
|
key={usp.id}
|
||||||
|
className="group text-center p-6 rounded-xl transition-all duration-300 hover-lift animate-fade-in-up"
|
||||||
|
style={{
|
||||||
|
background: 'var(--color-background)',
|
||||||
|
border: '1px solid var(--color-border)',
|
||||||
|
animationDelay: `${index * 100}ms`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="icon-container icon-container-lg mx-auto mb-4"
|
||||||
|
>
|
||||||
|
<span className="text-2xl">{usp.icon}</span>
|
||||||
|
</div>
|
||||||
|
<h3
|
||||||
|
className="text-lg font-semibold mb-2 transition-colors duration-200"
|
||||||
|
style={{ color: 'var(--color-foreground)' }}
|
||||||
|
>
|
||||||
|
{usp.title}
|
||||||
|
</h3>
|
||||||
|
<p style={{ color: 'var(--color-foreground-muted)' }}>
|
||||||
|
{usp.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Services Section */}
|
||||||
|
<section
|
||||||
|
className="py-20"
|
||||||
|
style={{ background: 'var(--color-background)' }}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-12">
|
||||||
|
<h2
|
||||||
|
className="text-3xl md:text-4xl font-bold mb-4"
|
||||||
|
style={{ color: 'var(--color-foreground)' }}
|
||||||
|
>
|
||||||
|
{pageContent.services.title}
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
className="text-lg max-w-3xl mx-auto"
|
||||||
|
style={{ color: 'var(--color-foreground-muted)' }}
|
||||||
|
>
|
||||||
|
{pageContent.services.subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
|
||||||
|
{pageContent.services.items.map((service, index) => (
|
||||||
|
<div
|
||||||
|
key={service.id}
|
||||||
|
className="group card hover-lift"
|
||||||
|
style={{ animationDelay: `${index * 100}ms` }}
|
||||||
|
>
|
||||||
|
<div className="icon-container mb-4">
|
||||||
|
<span className="text-xl">{service.icon}</span>
|
||||||
|
</div>
|
||||||
|
<h3
|
||||||
|
className="text-xl font-semibold mb-3 transition-colors duration-200 group-hover:text-blue-600"
|
||||||
|
style={{ color: 'var(--color-foreground)' }}
|
||||||
|
>
|
||||||
|
{service.title}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className="leading-relaxed mb-4"
|
||||||
|
style={{ color: 'var(--color-foreground-muted)' }}
|
||||||
|
>
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
<div className="mb-4">
|
||||||
|
<h4
|
||||||
|
className="text-sm font-semibold mb-2"
|
||||||
|
style={{ color: 'var(--color-foreground-secondary)' }}
|
||||||
|
>
|
||||||
|
{pageContent.serviceFeatures.title}
|
||||||
|
</h4>
|
||||||
|
<ul
|
||||||
|
className="text-sm space-y-1.5"
|
||||||
|
style={{ color: 'var(--color-foreground-muted)' }}
|
||||||
|
>
|
||||||
|
{service.features.map((feature, idx) => (
|
||||||
|
<li key={idx} className="flex items-start group/item">
|
||||||
|
<span
|
||||||
|
className="mr-2 transition-transform duration-200 group-hover/item:scale-125"
|
||||||
|
style={{ color: 'var(--color-success-500)' }}
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</span>
|
||||||
|
{feature}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={contactHref}
|
||||||
|
className="inline-flex items-center font-medium transition-all duration-200 group/link"
|
||||||
|
style={{ color: 'var(--color-primary-600)' }}
|
||||||
|
>
|
||||||
|
{service.ctaText}
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 ml-1 transition-transform duration-200 group-hover/link:translate-x-1"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Partners Section */}
|
||||||
|
{partners.length > 0 && (
|
||||||
|
<section className="py-16">
|
||||||
|
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
|
<h2 className="text-3xl font-bold text-center mb-3" style={{ color: 'var(--color-foreground)' }}>
|
||||||
|
{partnersCopy.title}
|
||||||
|
</h2>
|
||||||
|
<p className="text-center mb-10" style={{ color: 'var(--color-foreground-muted)' }}>
|
||||||
|
{partnersCopy.subtitle}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap justify-center items-center gap-10">
|
||||||
|
{partners.map((partner) => (
|
||||||
|
<a
|
||||||
|
key={partner.name}
|
||||||
|
href={partner.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="group flex flex-col items-center gap-3 opacity-80 hover:opacity-100 transition-opacity duration-200"
|
||||||
|
title={partner.name}
|
||||||
|
>
|
||||||
|
<span className="relative h-12 w-40">
|
||||||
|
<Image
|
||||||
|
src={partner.logo.url}
|
||||||
|
alt={partner.logo.alt}
|
||||||
|
fill
|
||||||
|
sizes="160px"
|
||||||
|
className="object-contain"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span className="text-sm" style={{ color: 'var(--color-foreground-muted)' }}>
|
||||||
|
{partner.name}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section
|
||||||
|
className="py-20 relative overflow-hidden"
|
||||||
|
style={{ background: 'var(--color-foreground)' }}
|
||||||
|
>
|
||||||
|
{/* Animated background elements */}
|
||||||
|
<div className="absolute inset-0 overflow-hidden pointer-events-none opacity-10">
|
||||||
|
<div
|
||||||
|
className="absolute top-10 left-10 w-32 h-32 rounded-full animate-float"
|
||||||
|
style={{ background: 'var(--color-primary-500)' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute bottom-10 right-20 w-24 h-24 rounded-full animate-float"
|
||||||
|
style={{ background: 'var(--color-accent-500)', animationDelay: '0.5s' }}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="absolute top-1/2 right-1/4 w-16 h-16 rounded-full animate-float"
|
||||||
|
style={{ background: 'var(--color-primary-400)', animationDelay: '1s' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center relative z-10">
|
||||||
|
<h2
|
||||||
|
className="text-3xl md:text-4xl font-bold mb-4"
|
||||||
|
style={{ color: 'var(--color-background)' }}
|
||||||
|
>
|
||||||
|
{pageContent.cta.title}
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
className="text-xl mb-8 max-w-2xl mx-auto"
|
||||||
|
style={{ color: 'var(--color-background)', opacity: 0.8 }}
|
||||||
|
>
|
||||||
|
{pageContent.cta.subtitle}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={contactHref}
|
||||||
|
className="btn btn-primary text-lg px-8 py-4 hover-glow"
|
||||||
|
>
|
||||||
|
{pageContent.cta.button}
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5 ml-2"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+15
-9
@@ -1,13 +1,19 @@
|
|||||||
import { content } from '@/content'
|
import type { Locale } from '@/lib/i18n'
|
||||||
|
import type { getLegalPage } from '@/lib/payload-content'
|
||||||
|
|
||||||
export const metadata = {
|
type LegalPageViewProps = {
|
||||||
title: `${content.pages.adatvedelem.title} | ${content.common.labels.features || 'mozdIT Bt.'}`,
|
page: NonNullable<Awaited<ReturnType<typeof getLegalPage>>>
|
||||||
description: 'Adatvédelmi tájékoztató - ismerje meg, hogyan kezeljük személyes adatait.',
|
locale: Locale
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function PrivacyPolicy() {
|
const LAST_UPDATED_LABEL: Record<Locale, string> = {
|
||||||
const pageContent = content.pages.adatvedelem
|
hu: 'Utolsó frissítés',
|
||||||
|
en: 'Last updated',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Közös nézet az adatvédelmi tájékoztatóhoz és a használati feltételekhez
|
||||||
|
* — mindkettő azonos szerkezetű (title/lastUpdated/sections). */
|
||||||
|
export default function LegalPageView({ page, locale }: LegalPageViewProps) {
|
||||||
return (
|
return (
|
||||||
<div className="py-20 lg:py-28 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="py-20 lg:py-28 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="mb-12">
|
<div className="mb-12">
|
||||||
@@ -15,18 +21,18 @@ export default function PrivacyPolicy() {
|
|||||||
className="text-4xl md:text-5xl font-bold mb-4"
|
className="text-4xl md:text-5xl font-bold mb-4"
|
||||||
style={{ color: 'var(--color-foreground)' }}
|
style={{ color: 'var(--color-foreground)' }}
|
||||||
>
|
>
|
||||||
{pageContent.title}
|
{page.title}
|
||||||
</h1>
|
</h1>
|
||||||
<p
|
<p
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
style={{ color: 'var(--color-foreground-muted)' }}
|
style={{ color: 'var(--color-foreground-muted)' }}
|
||||||
>
|
>
|
||||||
Utolsó frissítés: {pageContent.lastUpdated}
|
{LAST_UPDATED_LABEL[locale]}: {page.lastUpdated}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
{pageContent.sections.map((section) => (
|
{page.sections.map((section) => (
|
||||||
<section key={section.id}>
|
<section key={section.id}>
|
||||||
<h2
|
<h2
|
||||||
className="text-2xl font-semibold mb-4"
|
className="text-2xl font-semibold mb-4"
|
||||||
+20
-20
@@ -1,20 +1,16 @@
|
|||||||
import { siteConfig } from '@/config/site'
|
import { localePath, type Locale } from '@/lib/i18n'
|
||||||
import { content } from '@/content'
|
import type { getHomeContent, getServicesContent } from '@/lib/payload-content'
|
||||||
import type { Metadata } from 'next'
|
|
||||||
|
|
||||||
const { services: pageContent } = content.pages
|
type ServicesViewProps = {
|
||||||
|
content: Awaited<ReturnType<typeof getServicesContent>>
|
||||||
export const metadata: Metadata = {
|
homeServices: Awaited<ReturnType<typeof getHomeContent>>['services']['items']
|
||||||
title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
|
featuresLabel: string
|
||||||
description: pageContent.meta.description,
|
webmailHref: string
|
||||||
openGraph: {
|
locale: Locale
|
||||||
title: `${pageContent.meta.title} | ${siteConfig.general.name}`,
|
|
||||||
description: pageContent.meta.ogDescription,
|
|
||||||
url: `${siteConfig.general.url}/szolgaltatasok`,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ServicesPage() {
|
export default function ServicesView({ content: pageContent, homeServices, featuresLabel, webmailHref, locale }: ServicesViewProps) {
|
||||||
|
const contactHref = localePath(locale, 'contact')
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16 py-8">
|
<div className="space-y-16 py-8">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
@@ -32,7 +28,7 @@ export default function ServicesPage() {
|
|||||||
{/* Services Grid */}
|
{/* Services Grid */}
|
||||||
<section className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
<section className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||||
{content.pages.home.services.items.map((service) => (
|
{homeServices.map((service) => (
|
||||||
<div key={service.id} className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 hover:shadow-md transition-shadow">
|
<div key={service.id} className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 hover:shadow-md transition-shadow">
|
||||||
<div className="w-16 h-16 bg-blue-100 rounded-lg flex items-center justify-center mb-6">
|
<div className="w-16 h-16 bg-blue-100 rounded-lg flex items-center justify-center mb-6">
|
||||||
<span className="text-2xl">{service.icon}</span>
|
<span className="text-2xl">{service.icon}</span>
|
||||||
@@ -42,7 +38,7 @@ export default function ServicesPage() {
|
|||||||
<p className="text-gray-600 leading-relaxed mb-6">{service.description}</p>
|
<p className="text-gray-600 leading-relaxed mb-6">{service.description}</p>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<h3 className="text-lg font-semibold text-gray-900 mb-3">{content.common.labels.features}</h3>
|
<h3 className="text-lg font-semibold text-gray-900 mb-3">{featuresLabel}</h3>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{service.features.map((feature, index) => (
|
{service.features.map((feature, index) => (
|
||||||
<li key={index} className="flex items-start">
|
<li key={index} className="flex items-start">
|
||||||
@@ -54,7 +50,7 @@ export default function ServicesPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href="/kapcsolat"
|
href={contactHref}
|
||||||
className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium transition-colors"
|
className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium transition-colors"
|
||||||
>
|
>
|
||||||
{service.ctaText}
|
{service.ctaText}
|
||||||
@@ -115,7 +111,7 @@ export default function ServicesPage() {
|
|||||||
{pageContent.support.subtitle}
|
{pageContent.support.subtitle}
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 text-sm">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 text-sm">
|
||||||
{pageContent.support.channels.map((channel) => (
|
{(pageContent.support.channels ?? []).map((channel) => (
|
||||||
<div key={channel.title}>
|
<div key={channel.title}>
|
||||||
<h3 className="font-semibold text-gray-900 mb-2">{channel.title}</h3>
|
<h3 className="font-semibold text-gray-900 mb-2">{channel.title}</h3>
|
||||||
<p className="text-gray-600">{channel.description}</p>
|
<p className="text-gray-600">{channel.description}</p>
|
||||||
@@ -136,13 +132,17 @@ export default function ServicesPage() {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||||
<a
|
<a
|
||||||
href="/kapcsolat"
|
href={contactHref}
|
||||||
className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-medium px-8 py-3 rounded-md transition-colors"
|
className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-medium px-8 py-3 rounded-md transition-colors"
|
||||||
>
|
>
|
||||||
{pageContent.cta.primaryButton}
|
{pageContent.cta.primaryButton}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={content.pages.home.hero.cta.primary.href}
|
// WHY webmailHref és nem "/kapcsolat" ismét: a migráció előtti
|
||||||
|
// kód itt (bugként) a primary CTA hrefjét (/kapcsolat) használta
|
||||||
|
// target="_blank"-kal a "Webmail belépés" gombhoz — javítva a
|
||||||
|
// tényleges webmail URL-re.
|
||||||
|
href={webmailHref}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-block border-2 border-white text-white hover:bg-white hover:text-gray-900 font-medium px-8 py-3 rounded-md transition-colors"
|
className="inline-block border-2 border-white text-white hover:bg-white hover:text-gray-900 font-medium px-8 py-3 rounded-md transition-colors"
|
||||||
+62
-36
@@ -1,47 +1,73 @@
|
|||||||
import { SiteConfig } from '@/types/site'
|
import type { NavigationItem } from '@/types/site'
|
||||||
|
import { localePath, type Locale } from '@/lib/i18n'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Site Configuration - Centralized configuration for all public content
|
* Site Configuration - statikus, nyelvfüggetlen alapadatok.
|
||||||
* All content is easily modifiable without code changes
|
*
|
||||||
* This structure supports easy expansion for CMS integration later
|
* MITHOME-91/114: a navigáció és a lábláb jogi linkjei nyelvfüggővé váltak
|
||||||
|
* (a szlögök nyelvenként eltérnek — lásd src/lib/i18n.ts PAGE_SLUGS), ezért
|
||||||
|
* ezek most függvények, nem statikus tömbök. A navigáció-feliratok itt
|
||||||
|
* maradnak (nem Payload-tartalom) — ez sosem volt része a JSON content
|
||||||
|
* rendszernek, csak ez a config fájl, ezért a MITHOME-91 hatóköre ("JSON
|
||||||
|
* content rendszer kivezetése") nem érinti; a hu/en feliratpárok itt kézzel
|
||||||
|
* tartott, statikus fordítások.
|
||||||
*/
|
*/
|
||||||
export const siteConfig: SiteConfig = {
|
export const siteConfig = {
|
||||||
general: {
|
general: {
|
||||||
name: 'mozdIT Bt.',
|
name: 'mozdIT Bt.',
|
||||||
description: 'Megbízható web- és email szolgáltatás személyre szabott támogatással. Stabil tárhely, üzembiztos levelezés és DNS adminisztráció gyors reakcióval.',
|
|
||||||
url: process.env.NEXT_PUBLIC_SITE_URL || 'https://localhost:3000',
|
url: process.env.NEXT_PUBLIC_SITE_URL || 'https://localhost:3000',
|
||||||
ogImage: '/mozdit_logo_text.png',
|
ogImage: '/mozdit_logo_text.png',
|
||||||
locale: 'hu-HU'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
navigation: {
|
|
||||||
main: [
|
|
||||||
{ label: 'Kezdőlap', href: '/' },
|
|
||||||
{ label: 'Rólunk', href: '/rolunk' },
|
|
||||||
{ label: 'Szolgáltatások', href: '/szolgaltatasok' },
|
|
||||||
{ label: 'Kapcsolat', href: '/kapcsolat', primary: true }
|
|
||||||
],
|
|
||||||
footer: [
|
|
||||||
{ label: 'Kezdőlap', href: '/' },
|
|
||||||
{ label: 'Rólunk', href: '/rolunk' },
|
|
||||||
{ label: 'Szolgáltatások', href: '/szolgaltatasok' },
|
|
||||||
{ label: 'Kapcsolat', href: '/kapcsolat' },
|
|
||||||
{ label: 'Adatvédelmi tájékoztató', href: '/adatvedelem' },
|
|
||||||
{ label: 'Használati feltételek', href: '/felhasznalasi-feltetelek' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
footer: {
|
|
||||||
// Copyright text lives in content/common.json (CMS-editable) — only links remain here.
|
|
||||||
links: [
|
|
||||||
{ label: 'Adatvédelmi tájékoztató', href: '/adatvedelem' },
|
|
||||||
{ label: 'Használati feltételek', href: '/felhasznalasi-feltetelek' }
|
|
||||||
]
|
|
||||||
},
|
|
||||||
|
|
||||||
contact: {
|
contact: {
|
||||||
email: process.env.NEXT_PUBLIC_CONTACT_EMAIL || 'info@mozdit.hu',
|
email: process.env.NEXT_PUBLIC_CONTACT_EMAIL || 'info@mozdit.hu',
|
||||||
// address lives in content/common.json (footer.address) — CMS-editable.
|
// address lives in the Payload Common global (footer.address) — CMS-editable.
|
||||||
// The form fields live in content/pages/contact.json.
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DESCRIPTIONS: Record<Locale, string> = {
|
||||||
|
hu: 'Megbízható web- és email szolgáltatás személyre szabott támogatással. Stabil tárhely, üzembiztos levelezés és DNS adminisztráció gyors reakcióval.',
|
||||||
|
en: 'Reliable web hosting and business email with personal support. Stable hosting, dependable mail delivery and DNS administration with a fast response.',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSiteDescription(locale: Locale): string {
|
||||||
|
return DESCRIPTIONS[locale]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOgLocale(locale: Locale): string {
|
||||||
|
return locale === 'hu' ? 'hu-HU' : 'en-US'
|
||||||
|
}
|
||||||
|
|
||||||
|
const NAV_LABELS: Record<Locale, { home: string; about: string; services: string; contact: string; privacy: string; terms: string }> = {
|
||||||
|
hu: { home: 'Kezdőlap', about: 'Rólunk', services: 'Szolgáltatások', contact: 'Kapcsolat', privacy: 'Adatvédelmi tájékoztató', terms: 'Használati feltételek' },
|
||||||
|
en: { home: 'Home', about: 'About', services: 'Services', contact: 'Contact', privacy: 'Privacy Policy', terms: 'Terms of Service' },
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMainNavigation(locale: Locale): NavigationItem[] {
|
||||||
|
const t = NAV_LABELS[locale]
|
||||||
|
return [
|
||||||
|
{ label: t.home, href: localePath(locale) },
|
||||||
|
{ label: t.about, href: localePath(locale, 'about') },
|
||||||
|
{ label: t.services, href: localePath(locale, 'services') },
|
||||||
|
{ label: t.contact, href: localePath(locale, 'contact'), primary: true },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFooterNavigation(locale: Locale): NavigationItem[] {
|
||||||
|
const t = NAV_LABELS[locale]
|
||||||
|
return [
|
||||||
|
{ label: t.home, href: localePath(locale) },
|
||||||
|
{ label: t.about, href: localePath(locale, 'about') },
|
||||||
|
{ label: t.services, href: localePath(locale, 'services') },
|
||||||
|
{ label: t.contact, href: localePath(locale, 'contact') },
|
||||||
|
{ label: t.privacy, href: localePath(locale, 'privacy') },
|
||||||
|
{ label: t.terms, href: localePath(locale, 'terms') },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFooterLegalLinks(locale: Locale): NavigationItem[] {
|
||||||
|
const t = NAV_LABELS[locale]
|
||||||
|
return [
|
||||||
|
{ label: t.privacy, href: localePath(locale, 'privacy') },
|
||||||
|
{ label: t.terms, href: localePath(locale, 'terms') },
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"errorMinLength": "Az üzenet legalább 10 karakter hosszú legyen"
|
"errorMinLength": "Az üzenet legalább 10 karakter hosszú legyen"
|
||||||
},
|
},
|
||||||
"gdpr": {
|
"gdpr": {
|
||||||
"label": "Elfogadom az <a href=\"/adatkezelesi-tajekoztato\" class=\"text-blue-600 hover:text-blue-700 underline\">adatkezelési tájékoztatót</a> és hozzájárulok személyes adataim kezeléséhez a kapcsolatfelvétel céljából.",
|
"label": "Elfogadom az <a href=\"{privacyHref}\" class=\"text-blue-600 hover:text-blue-700 underline\">adatkezelési tájékoztatót</a> és hozzájárulok személyes adataim kezeléséhez a kapcsolatfelvétel céljából.",
|
||||||
"error": "Az adatkezelési tájékoztató elfogadása kötelező"
|
"error": "Az adatkezelési tájékoztató elfogadása kötelező"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/**
|
||||||
|
* MITHOME-91/114: locale + útvonal segédfüggvények.
|
||||||
|
*
|
||||||
|
* URL-stratégia (2026-09-10, felülírja a korábbi "hu prefix nélkül" döntést):
|
||||||
|
* mindkét nyelv prefixet kap, szimmetrikusan (`/hu/...`, `/en/...`), a
|
||||||
|
* szlögök nyelvenként lefordítva (pl. /hu/rolunk vs /en/about). Nincs
|
||||||
|
* redirect a régi, prefix nélküli URL-ekről — a projekt még nincs
|
||||||
|
* production-ben (MITHOME-15 backlog).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const LOCALES = ['hu', 'en'] as const
|
||||||
|
export type Locale = (typeof LOCALES)[number]
|
||||||
|
export const DEFAULT_LOCALE: Locale = 'hu'
|
||||||
|
|
||||||
|
export function isLocale(value: string): value is Locale {
|
||||||
|
return (LOCALES as readonly string[]).includes(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A catch-all [slug] route alá tartozó oldalak azonosítói. */
|
||||||
|
export type PageKey = 'about' | 'services' | 'contact' | 'privacy' | 'terms'
|
||||||
|
|
||||||
|
/** Nyelvenkénti szlög minden oldalhoz — a nyelvváltó (MITHOME-115) és a
|
||||||
|
* sitemap/hreflang (MITHOME-116) is ezt a táblát fogja használni. */
|
||||||
|
export const PAGE_SLUGS: Record<PageKey, Record<Locale, string>> = {
|
||||||
|
about: { hu: 'rolunk', en: 'about' },
|
||||||
|
services: { hu: 'szolgaltatasok', en: 'services' },
|
||||||
|
contact: { hu: 'kapcsolat', en: 'contact' },
|
||||||
|
privacy: { hu: 'adatvedelem', en: 'privacy-policy' },
|
||||||
|
terms: { hu: 'felhasznalasi-feltetelek', en: 'terms-of-service' },
|
||||||
|
}
|
||||||
|
|
||||||
|
/** slug -> PageKey visszakeresés egy adott nyelven belül. */
|
||||||
|
export function resolvePageKey(locale: Locale, slug: string): PageKey | undefined {
|
||||||
|
return (Object.keys(PAGE_SLUGS) as PageKey[]).find((key) => PAGE_SLUGS[key][locale] === slug)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Útvonal a főoldalhoz vagy egy PageKey-hez, adott nyelven. */
|
||||||
|
export function localePath(locale: Locale, key?: PageKey): string {
|
||||||
|
if (!key) return `/${locale}`
|
||||||
|
return `/${locale}/${PAGE_SLUGS[key][locale]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ugyanaz az oldal a másik nyelven — a nyelvváltóhoz (MITHOME-115).
|
||||||
|
* Ha a jelenlegi útvonal nem ismert PageKey (pl. 404), a másik nyelv
|
||||||
|
* főoldalára esik vissza.
|
||||||
|
*/
|
||||||
|
export function switchLocalePath(currentLocale: Locale, targetLocale: Locale, slug?: string): string {
|
||||||
|
if (!slug) return localePath(targetLocale)
|
||||||
|
const key = resolvePageKey(currentLocale, slug)
|
||||||
|
return localePath(targetLocale, key)
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
/**
|
||||||
|
* MITHOME-91: Payload Local API adat-adapter réteg.
|
||||||
|
*
|
||||||
|
* WHY adapterek: a Payload Globals/Collections mezői (stringArrayField,
|
||||||
|
* lásd src/globals/fields/stringArray.ts) `{ value: string }[]` alakban
|
||||||
|
* tárolják azt, ami a JSON content rendszerben egyszerű `string[]` volt.
|
||||||
|
* Ezek a getterek visszaadaptálják az eredeti alakra, hogy a page
|
||||||
|
* komponensek JSX-e (ami a régi content/types.ts formát várja) NE
|
||||||
|
* változzon — csak az adatforrás.
|
||||||
|
*/
|
||||||
|
import { getPayload } from 'payload'
|
||||||
|
import config from '@payload-config'
|
||||||
|
import type { Locale } from './i18n'
|
||||||
|
|
||||||
|
let cached: ReturnType<typeof getPayload> | undefined
|
||||||
|
|
||||||
|
function payloadClient() {
|
||||||
|
cached ??= getPayload({ config })
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
function unwrap(items: readonly { value: string }[] | null | undefined): string[] {
|
||||||
|
return (items ?? []).map((item) => item.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCommonContent(locale: Locale) {
|
||||||
|
const payload = await payloadClient()
|
||||||
|
return payload.findGlobal({ slug: 'common', locale })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHomeContent(locale: Locale) {
|
||||||
|
const payload = await payloadClient()
|
||||||
|
const home = await payload.findGlobal({ slug: 'home', locale })
|
||||||
|
return {
|
||||||
|
...home,
|
||||||
|
hero: { ...home.hero, trustBullets: unwrap(home.hero?.trustBullets) },
|
||||||
|
services: {
|
||||||
|
...home.services,
|
||||||
|
items: (home.services?.items ?? []).map((item) => ({
|
||||||
|
...item,
|
||||||
|
features: unwrap(item.features),
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAboutContent(locale: Locale) {
|
||||||
|
const payload = await payloadClient()
|
||||||
|
const about = await payload.findGlobal({ slug: 'about', locale })
|
||||||
|
return {
|
||||||
|
...about,
|
||||||
|
story: { ...about.story, paragraphs: unwrap(about.story?.paragraphs) },
|
||||||
|
team: { ...about.team, paragraphs: unwrap(about.team?.paragraphs) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getServicesContent(locale: Locale) {
|
||||||
|
const payload = await payloadClient()
|
||||||
|
const services = await payload.findGlobal({ slug: 'services', locale })
|
||||||
|
return {
|
||||||
|
...services,
|
||||||
|
details: {
|
||||||
|
...services.details,
|
||||||
|
services: (services.details?.services ?? []).map((service) => ({
|
||||||
|
...service,
|
||||||
|
specs: { ...service.specs, items: unwrap(service.specs?.items) },
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getContactContent(locale: Locale) {
|
||||||
|
const payload = await payloadClient()
|
||||||
|
return payload.findGlobal({ slug: 'contact', locale })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLegalPage(slug: 'adatvedelem' | 'hasznalati-feltetelek', locale: Locale) {
|
||||||
|
const payload = await payloadClient()
|
||||||
|
const result = await payload.find({
|
||||||
|
collection: 'legal-pages',
|
||||||
|
where: { slug: { equals: slug } },
|
||||||
|
locale,
|
||||||
|
limit: 1,
|
||||||
|
})
|
||||||
|
return result.docs[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PartnerView = { name: string; url: string; logo: { url: string; alt: string } }
|
||||||
|
|
||||||
|
export async function getPartners(): Promise<PartnerView[]> {
|
||||||
|
const payload = await payloadClient()
|
||||||
|
const result = await payload.find({ collection: 'partners', limit: 100, sort: 'name' })
|
||||||
|
return result.docs
|
||||||
|
.filter((doc) => typeof doc.logo === 'object' && doc.logo?.url)
|
||||||
|
.map((doc) => ({
|
||||||
|
name: doc.name,
|
||||||
|
url: doc.url,
|
||||||
|
logo: {
|
||||||
|
url: (doc.logo as { url: string }).url,
|
||||||
|
alt: (doc.logo as { alt?: string }).alt ?? doc.name,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user