feat: add allowed Linear API operations to MCP config

This commit is contained in:
Do Siki
2025-09-05 03:10:17 +02:00
parent 7e7d3fb1cc
commit b6365543ef
36 changed files with 14648 additions and 1 deletions
+85
View File
@@ -0,0 +1,85 @@
import { GET, HEAD } from './route'
import { NextRequest } from 'next/server'
// Mock the process object for tests
const mockProcess = {
uptime: jest.fn().mockReturnValue(1234),
env: {
npm_package_version: '1.0.0',
NODE_ENV: 'test',
},
}
const originalProcess = global.process
beforeEach(() => {
global.process = { ...originalProcess, ...mockProcess } as any
})
afterEach(() => {
global.process = originalProcess
})
describe('/api/health', () => {
describe('GET request', () => {
it('should return successful health status with required data', async () => {
const response = await GET()
// Check response status
expect(response.status).toBe(200)
// Get JSON data
const data = await response.json()
// Verify required fields
expect(data).toHaveProperty('status', 'ok')
expect(data).toHaveProperty('timestamp')
expect(data).toHaveProperty('uptime')
expect(data).toHaveProperty('version')
expect(data).toHaveProperty('environment')
// Verify timestamp is a valid ISO string
expect(() => new Date(data.timestamp)).not.toThrow()
expect(data.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/)
// Verify uptime matches mock
expect(data.uptime).toBe(1234)
// Verify version
expect(data.version).toBe('1.0.0')
})
it('should include correct Cache-Control headers', async () => {
const response = await GET()
expect(response.headers.get('Cache-Control')).toBe('no-cache, no-store, must-revalidate')
expect(response.headers.get('Pragma')).toBe('no-cache')
expect(response.headers.get('Expires')).toBe('0')
})
it('should handle errors gracefully', async () => {
// Temporarily break process.env to simulate error
global.process.env = undefined as any
const response = await GET()
expect(response.status).toBe(503)
const data = await response.json()
expect(data.status).toBe('error')
expect(data).toHaveProperty('timestamp')
expect(data).toHaveProperty('message', 'Health check failed')
// Restore process.env
global.process.env = originalProcess.env
})
})
describe('HEAD request', () => {
it('should return 200 status without body', async () => {
const response = await HEAD()
expect(response.status).toBe(200)
expect(response.headers.get('Cache-Control')).toBe('no-cache, no-store, must-revalidate')
})
})
})
+43
View File
@@ -0,0 +1,43 @@
import { NextResponse } from 'next/server';
export async function GET() {
try {
// Basic health check
const healthData = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development',
};
return NextResponse.json(healthData, {
status: 200,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
},
});
} catch (error) {
console.error('Health check error:', error);
return NextResponse.json(
{
status: 'error',
timestamp: new Date().toISOString(),
message: 'Health check failed',
},
{ status: 503 }
);
}
}
// Also support HEAD requests for lighter health checks
export async function HEAD() {
return new Response(null, {
status: 200,
headers: {
'Cache-Control': 'no-cache, no-store, must-revalidate',
},
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+26
View File
@@ -0,0 +1,26 @@
@import "tailwindcss";
:root {
--background: #ffffff;
--foreground: #171717;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);
font-family: Arial, Helvetica, sans-serif;
}
+76
View File
@@ -0,0 +1,76 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Header from "../components/Header";
import Footer from "../components/Footer";
import { siteConfig } from "../config/site";
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: `${siteConfig.general.name} | ${siteConfig.general.description}`,
description: siteConfig.general.description,
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,
},
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="hu">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen flex flex-col`}
>
<Header />
<main className="flex-1">
{children}
</main>
<Footer />
</body>
</html>
);
}
+120
View File
@@ -0,0 +1,120 @@
import { siteConfig } from '@/config/site'
export default function Home() {
return (
<div className="space-y-16">
{/* Hero Section */}
<section className="bg-gradient-to-r from-blue-50 to-indigo-50 py-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 mb-6 leading-tight">
{siteConfig.hero.title}
</h1>
<p className="text-lg md:text-xl text-gray-600 max-w-4xl mx-auto mb-8 leading-relaxed">
{siteConfig.hero.description}
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<a
href={siteConfig.hero.cta.primary.href}
target={siteConfig.hero.cta.primary.external ? '_blank' : undefined}
rel={siteConfig.hero.cta.primary.external ? 'noopener noreferrer' : undefined}
className="bg-blue-600 hover:bg-blue-700 text-white font-medium px-8 py-4 rounded-md transition-colors text-lg inline-flex items-center gap-2"
>
<svg className="w-5 h-5" 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>
{siteConfig.hero.cta.primary.text}
</a>
{siteConfig.hero.cta.secondary && (
<a
href={siteConfig.hero.cta.secondary.href}
className="border-2 border-blue-600 text-blue-600 hover:bg-blue-50 font-medium px-8 py-4 rounded-md transition-colors text-lg"
>
{siteConfig.hero.cta.secondary.text}
</a>
)}
</div>
</div>
</section>
{/* USP Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 bg-gray-50">
<div className="text-center">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-8">
{siteConfig.about.title}
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
{siteConfig.about.usps.map((usp) => (
<div key={usp.id} className="text-center">
<div className="w-16 h-16 bg-blue-100 group-hover:bg-blue-200 rounded-full flex items-center justify-center mx-auto mb-4 transition-colors">
<span className="text-2xl">{usp.icon}</span>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">{usp.title}</h3>
<p className="text-gray-600">{usp.description}</p>
</div>
))}
</div>
</div>
</section>
{/* Services Section */}
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold text-gray-900 mb-6">
{siteConfig.services.title}
</h2>
<p className="text-lg text-gray-600 max-w-3xl mx-auto">
{siteConfig.services.subtitle}
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
{siteConfig.services.services.map((service) => (
<div key={service.id} className="group bg-white p-8 rounded-xl shadow-sm border border-gray-200 hover:shadow-md transition-all duration-300 hover:border-blue-200">
<div className="w-12 h-12 bg-blue-100 group-hover:bg-blue-200 rounded-lg flex items-center justify-center mb-4 transition-colors">
<span className="text-xl">{service.icon}</span>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3 group-hover:text-blue-600 transition-colors">{service.title}</h3>
<p className="text-gray-600 leading-relaxed">
{service.description}
</p>
<div className="mt-4">
<h4 className="text-sm font-semibold text-gray-700 mb-2">Szolgáltatás jellemzők:</h4>
<ul className="text-sm text-gray-600 space-y-1">
{service.features.map((feature, index) => (
<li key={index} className="flex items-start">
<span className="text-blue-500 mr-2"></span>
{feature}
</li>
))}
</ul>
</div>
<a href="/kapcsolat" className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium mt-4 transition-colors">
{service.ctaText}
<svg className="w-4 h-4 ml-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>
</section>
{/* CTA Section */}
<section className="bg-gray-900 text-white py-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h2 className="text-3xl font-bold mb-4">
Kapcsolatfelvétel az első lépés
</h2>
<p className="text-xl text-gray-300 mb-8">
Mutassuk meg, hogyan segíthetünk Önnek megvalósítani címeit!
</p>
<a
href="/kapcsolat"
className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-medium px-8 py-3 rounded-md transition-colors"
>
Kapcsolatfelvétel
</a>
</div>
</section>
</div>
);
}
+73
View File
@@ -0,0 +1,73 @@
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import Footer from './Footer'
describe('Footer', () => {
it('should render company information', () => {
render(<Footer />)
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()
})
it('should render email and company details', () => {
render(<Footer />)
expect(screen.getByText('Email: info@mozdit.hu')).toBeInTheDocument()
expect(screen.getByText('Cég: mozdIT Bt.')).toBeInTheDocument()
expect(screen.getByText('Székhely: Budapest, Magyarország')).toBeInTheDocument()
})
it('should render navigation links', () => {
render(<Footer />)
expect(screen.getByText('Kezdőlap')).toBeInTheDocument()
expect(screen.getByText('Rólunk')).toBeInTheDocument()
expect(screen.getAllByText('Szolgáltatások')).toHaveLength(2) // Both in navigation and services section
expect(screen.getByText('Kapcsolat')).toBeInTheDocument()
})
it('should render service sections', () => {
render(<Footer />)
expect(screen.getByText('Web Hosting')).toBeInTheDocument()
expect(screen.getByText('Email Szolgáltatás')).toBeInTheDocument()
expect(screen.getByText('DNS Adminisztráció')).toBeInTheDocument()
expect(screen.getByText('Műszaki támogatás')).toBeInTheDocument()
})
it('should render copyright notice with dynamic year', () => {
render(<Footer />)
const currentYear = new Date().getFullYear()
expect(screen.getByText(`© ${currentYear} mozdIT Bt. Minden jog fenntartva.`)).toBeInTheDocument()
})
it('should render legal links', () => {
render(<Footer />)
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
})
it('should render with proper grid layout', () => {
const { container } = render(<Footer />)
const gridContainer = container.querySelector('.grid.grid-cols-1.md\\:grid-cols-4')
expect(gridContainer).toBeInTheDocument()
// Check for responsive grid classes
expect(gridContainer).toHaveClass('grid-cols-1', 'md:grid-cols-4')
})
it('should render with proper semantic structure', () => {
const { container } = render(<Footer />)
// Should have a footer element
const footer = container.firstChild as HTMLElement
expect(footer?.tagName).toBe('FOOTER')
// Should have proper background and padding
expect(footer).toHaveClass('bg-gray-50', 'border-t')
})
})
+79
View File
@@ -0,0 +1,79 @@
import { siteConfig } from '@/config/site'
export default function Footer() {
return (
<footer className="bg-gray-50 border-t border-gray-200">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
{/* Company Info */}
<div className="md:col-span-2">
<h3 className="text-lg font-semibold text-gray-900 mb-4">
{siteConfig.general.name}
</h3>
<p className="text-gray-600 mb-4 max-w-md">
{siteConfig.general.description}
</p>
<div className="flex space-x-4">
<div className="text-sm text-gray-500">
<p>Email: {siteConfig.contact.email}</p>
<p>Cég: {siteConfig.general.name}</p>
<p>Székhely: {siteConfig.contact.address}</p>
</div>
</div>
</div>
{/* Navigation Links */}
<div>
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide mb-4">
Navigáció
</h3>
<ul className="space-y-2">
{siteConfig.navigation.footer.map((item) => (
<li key={item.href}>
<a href={item.href} className="text-gray-600 hover:text-blue-600 transition-colors">
{item.label}
</a>
</li>
))}
</ul>
</div>
{/* Services */}
<div>
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide mb-4">
Szolgáltatások
</h3>
<ul className="space-y-2">
{siteConfig.services.services.map((service) => (
<li key={service.id} className="text-gray-600">
{service.title}
</li>
))}
<li className="text-gray-600">Műszaki támogatás</li>
</ul>
</div>
</div>
{/* Bottom section */}
<div className="border-t border-gray-200 pt-8 mt-8">
<div className="flex flex-col sm:flex-row justify-between items-center">
<p className="text-gray-500 text-sm">
{siteConfig.footer.copyright}
</p>
<div className="flex space-x-4 mt-4 sm:mt-0">
{siteConfig.footer.links.map((link) => (
<a
key={link.href}
href={link.href}
className="text-gray-500 hover:text-blue-600 text-sm transition-colors"
>
{link.label}
</a>
))}
</div>
</div>
</div>
</div>
</footer>
);
}
+80
View File
@@ -0,0 +1,80 @@
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom'
import Header from './Header'
import userEvent from '@testing-library/user-event'
// Mock Next.js Link component
jest.mock('next/link', () => {
return ({ children, href }: { children: React.ReactNode; href: string }) => (
<a href={href}>{children}</a>
)
})
describe('Header', () => {
it('should render the company logo', () => {
render(<Header />)
expect(screen.getByText('mozdIT Bt.')).toBeInTheDocument()
})
it('should render all navigation links in desktop menu', () => {
render(<Header />)
// Desktop menu should contain all links with specific structures
const desktopMenu = document.querySelector('.hidden.md\\:block')
expect(desktopMenu).toBeInTheDocument()
const navLinks = screen.getAllByText('Kezdőlap')
expect(navLinks.length).toBeGreaterThan(0)
expect(screen.getAllByText('Rólunk')).toHaveLength(2) // Both in desktop and mobile menus
expect(screen.getAllByText('Szolgáltatások')).toHaveLength(2) // Both in desktop and mobile menus
})
it('should render contact button with correct styling', () => {
render(<Header />)
const contactButtons = screen.getAllByText('Kapcsolat')
expect(contactButtons.length).toBeGreaterThan(0)
// Check if any contact button has the correct styling
const contactButton = contactButtons[0]
expect(contactButton).toBeInTheDocument()
// Check for blue background styling
const contactLink = contactButton.closest('a')
if (contactLink) {
expect(contactLink).toHaveClass('bg-blue-600')
}
})
it('should render hamburger menu button on mobile', () => {
render(<Header />)
// The hamburger menu button is hidden by default in desktop view
// We can test its presence even if not visible
const hamburgerButton = screen.getByRole('button')
expect(hamburgerButton).toBeInTheDocument()
})
it('should have proper accessibility attributes', () => {
render(<Header />)
const hamburgerButton = screen.getByRole('button')
expect(hamburgerButton).toHaveAttribute('aria-expanded', 'false')
})
it('should render with proper semantic structure', () => {
const { container } = render(<Header />)
// Should have header element with proper structure
const header = container.firstChild as HTMLElement
expect(header?.tagName).toBe('HEADER')
// Should have a nav element
const nav = container.querySelector('nav')
expect(nav).toBeInTheDocument()
// Should be sticky positioned
expect(header).toHaveClass('sticky', 'top-0')
})
})
+70
View File
@@ -0,0 +1,70 @@
import { siteConfig } from '@/config/site'
export default function Header() {
return (
<header className="bg-white border-b border-gray-200 sticky top-0 z-50">
<nav className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<div className="flex-shrink-0">
<a href="/" className="text-xl font-bold text-blue-600 hover:text-blue-700">
{siteConfig.general.name}
</a>
</div>
{/* Desktop Navigation */}
<div className="hidden md:block">
<div className="flex items-center space-x-8">
{siteConfig.navigation.main.map((item) => (
<a
key={item.href}
href={item.href}
target={item.external ? '_blank' : undefined}
rel={item.external ? 'noopener noreferrer' : undefined}
className={item.label === 'Kapcsolat'
? "bg-blue-600 text-white px-4 py-2 rounded-md text-sm font-medium hover:bg-blue-700 transition-colors"
: "text-gray-900 hover:text-blue-600 px-3 py-2 text-sm font-medium transition-colors"
}
>
{item.label}
</a>
))}
</div>
</div>
{/* Mobile menu button */}
<div className="md:hidden">
<button
type="button"
className="text-gray-500 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
aria-expanded="false"
>
<span className="sr-only">Open main menu</span>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</div>
</div>
{/* Mobile Navigation - Hidden by default */}
<div className="md:hidden absolute top-full left-0 right-0 bg-white border-b border-gray-200 shadow-lg opacity-0 invisible transition-all duration-300 ease-in-out">
<div className="px-2 pt-2 pb-3 space-y-1">
<a href="/" className="block px-3 py-2 rounded-md text-base font-medium text-gray-900 hover:text-blue-600">
Kezdőlap
</a>
<a href="/rolunk" className="block px-3 py-2 rounded-md text-base font-medium text-gray-900 hover:text-blue-600">
Rólunk
</a>
<a href="/szolgaltatasok" className="block px-3 py-2 rounded-md text-base font-medium text-gray-900 hover:text-blue-600">
Szolgáltatások
</a>
<a href="/kapcsolat" className="block px-3 py-2 rounded-md text-base font-medium bg-blue-600 text-white">
Kapcsolat
</a>
</div>
</div>
</nav>
</header>
);
}
+172
View File
@@ -0,0 +1,172 @@
import { SiteConfig } from '@/types/site'
/**
* Site Configuration - Centralized configuration for all public content
* All content is easily modifiable without code changes
* This structure supports easy expansion for CMS integration later
*/
export const siteConfig: SiteConfig = {
general: {
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',
ogImage: '/og-image.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' }
],
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' }
]
},
hero: {
title: 'Megbízható web és emailszolgáltatás személyre szabott támogatással',
subtitle: 'Kis ügyfélkör, nagy figyelem: stabil tárhely, üzembiztos levelezés és DNS adminisztráció — gyors reakcióval.',
description: 'A mozdIT Bt. célja, hogy megbízható, támogató szolgáltatásokkal segítse ügyfeleit a digitális térben való sikerben.',
cta: {
primary: {
text: 'Webmail Ugrás',
href: process.env.NEXT_PUBLIC_WEBMAIL_URL || 'https://webmail.mozdit.hu',
external: true
},
secondary: {
text: 'Kapcsolatfelvétel',
href: '/kapcsolat'
}
}
},
services: {
title: 'Szolgáltatásaink',
subtitle: 'Komplex megoldásokat kínálunk, amelyek tökéletesen igazodnak a vállalkozás igényeihez.',
services: [
{
id: 'hosting',
title: 'Web Hosting',
description: 'Stabil és gyors tárhely szolgáltatás megbízható infrastrukturával és folyamatos monitoringgel.',
icon: '🔥',
features: [
'99.9% uptime garancia',
'SSD tárhely gyorsabb válaszidőért',
'24/7 monitoring és támogatás',
'Automatikus backup rendszer',
'SSL tanúsítvány belefoglaltva'
],
ctaText: 'További információk'
},
{
id: 'email',
title: 'Email Szolgáltatás',
description: 'Üzembiztos levelezési megoldások modern biztonsággal és anti-spam védelemmel.',
icon: '📧',
features: [
'Domain alapú email címe',
'Webmail és IMAP/POP3 támogatása',
'Anti-spam és anti-virus védelem',
'Mobil szinkronizáció',
'Nagy tárterület 10GB/felhasználó'
],
ctaText: 'További információk'
},
{
id: 'dns',
title: 'DNS Adminisztráció',
description: 'Teljeskörű domain névszerver kezelés és optimalizálás gyors névfeloldással.',
icon: '🌐',
features: [
'Biztonságos DNS konfliktus kezelés',
'Email routing konfiguráció',
'Subdomain management',
'DNSSEC támogatás',
'FIGYELEM: Senki ne módosítson nélkülem kérem!'
],
ctaText: 'További információk'
}
]
},
about: {
title: 'Miért érdemes minket választani?',
description: [
'A mozdIT Bt. 2018 óta nyújt megbízható Web Hosting szolgáltatásokat magyar vállalkozások számára.',
'Kis ügyfélközpontú csapatunknak köszönhetően személyre szabott és gyors támogatást tudunk biztosítani.'
],
usps: [
{
id: 'personal',
title: 'Személyes ügyfélkezelés',
description: 'Egyedi figyelem minden ügyfél felé, személyre szabott megoldásokkal.',
icon: '👤'
},
{
id: 'fast',
title: 'Gyors reagálás',
description: 'Azonnali visszajelzés és hatékony problémamegoldás 24 órás támogatással.',
icon: '⚡'
},
{
id: 'reliable',
title: 'Stabil háttér',
description: 'Biztonságos infrastruktúra és rendszeres mentések, megbízhatóság garanciával.',
icon: '🛡️'
},
{
id: 'flexible',
title: 'Rugalmas támogatás',
description: 'A változó igényekhez alkalmazkodó, örökös technikai karbantartás.',
icon: '🔧'
}
]
},
footer: {
copyright: `© ${new Date().getFullYear()} mozdIT Bt. Minden jog fenntartva.`,
links: [
{ label: 'Adatvédelmi tájékoztató', href: '/adatvedelem' },
{ label: 'Használati feltételek', href: '/felhasznalasi-feltetelek' }
]
},
contact: {
email: process.env.NEXT_PUBLIC_CONTACT_EMAIL || 'info@mozdit.hu',
address: 'Budapest, Magyarország',
form: {
title: 'Kapcsolatfelvétel',
description: 'Legyen szíves érdeklődését vagy problémáját részletesen megfogalmazni.',
submitText: 'Üzenet küldése',
fields: {
name: {
label: 'Név',
placeholder: 'Vezetéknév Keresztnév',
required: true
},
email: {
label: 'Email cím',
placeholder: 'pelda@email.hu',
required: true
},
message: {
label: 'Üzenet',
placeholder: 'Kérjük írja le érdeklődését részletesen...',
required: true
},
consent: {
label: 'Elfogadom az adatkezelési tájékoztatót',
required: true
}
}
}
}
}
+197
View File
@@ -0,0 +1,197 @@
import winston from 'winston'
// Mock winston and winston-loki
jest.mock('winston', () => ({
format: {
combine: jest.fn(),
timestamp: jest.fn(),
errors: jest.fn(),
json: jest.fn(),
colorize: jest.fn(),
simple: jest.fn(),
printf: jest.fn()
},
transports: {
Console: jest.fn(),
File: jest.fn()
},
createLogger: jest.fn()
}))
jest.mock('winston-loki', () => jest.fn())
describe('Logger', () => {
beforeEach(() => {
jest.clearAllMocks()
// Reset process.env
process.env = {
...process.env,
NODE_ENV: 'test',
LOKI_HOST: undefined,
LOKI_USERNAME: undefined,
LOKI_PASSWORD: undefined
}
})
it('should create logger with correct configuration', () => {
// Mock the format functions
const mockFormat = {
combine: jest.fn().mockReturnValue('combined-format'),
timestamp: jest.fn().mockReturnValue('timestamp-format'),
errors: jest.fn().mockReturnValue('errors-format'),
json: jest.fn().mockReturnValue('json-format'),
colorize: jest.fn().mockReturnValue('colorize-format'),
simple: jest.fn().mockReturnValue('simple-format'),
printf: jest.fn().mockReturnValue('printf-format')
}
const mockTransports = {
Console: jest.fn().mockImplementation(() => ({ name: 'console' })),
File: jest.fn().mockImplementation(() => ({ name: 'file' }))
}
// Setup mocks
;(winston.format as any) = mockFormat
;(winston.transports as any) = mockTransports
;(winston.createLogger as jest.Mock) = jest.fn().mockReturnValue({
info: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
warn: jest.fn(),
child: jest.fn().mockReturnValue({
info: jest.fn(),
error: jest.fn(),
debug: jest.fn()
}),
end: jest.fn()
})
// Import after mocks are set up
const { logger } = require('./logger')
expect(winston.createLogger).toHaveBeenCalledWith(
expect.objectContaining({
level: 'debug',
format: 'combined-format',
defaultMeta: expect.objectContaining({
service: 'mozdit-web',
environment: 'test'
}),
transports: expect.arrayContaining([
expect.objectContaining({ name: 'console' })
])
})
)
})
it('should include Loki transport when LOKI_HOST is provided', () => {
// Set LOKI_HOST
process.env.LOKI_HOST = 'http://localhost:3100'
process.env.LOKI_USERNAME = 'test'
process.env.LOKI_PASSWORD = 'testpass'
// Reset modules to pick up new env vars
jest.resetModules()
// Mock LokiTransport
const mockLokiTransport = jest.fn().mockImplementation(() => ({
name: 'loki'
}))
jest.doMock('winston-loki', () => mockLokiTransport)
// Import after mocks are set up
require('./logger')
expect(mockLokiTransport).toHaveBeenCalledWith(
expect.objectContaining({
host: 'http://localhost:3100',
labels: expect.objectContaining({
app: 'mozdit-web',
environment: 'test',
service: 'frontend'
}),
basicAuth: 'test:testpass'
})
)
})
it('should create component loggers with correct metadata', () => {
// Setup mocks
const mockLogger = {
child: jest.fn().mockReturnValue({
info: jest.fn(),
error: jest.fn()
})
}
;(winston.createLogger as jest.Mock) = jest.fn().mockReturnValue(mockLogger)
// Import after mocks are set up
const { createComponentLogger } = require('./logger')
const componentLogger = createComponentLogger('test-component')
expect(mockLogger.child).toHaveBeenCalledWith({ component: 'test-component' })
expect(componentLogger).toBeDefined()
})
it('should generate request IDs in correct format', () => {
// Setup mocks
jest.resetModules()
const { generateRequestId } = require('./logger')
const requestId = generateRequestId()
expect(requestId).toMatch(/^\d+-[a-z0-9]+$/)
})
it('should handle timing operations correctly', async () => {
// Setup mocks
const mockLogger = {
debug: jest.fn(),
error: jest.fn()
}
;(winston.createLogger as jest.Mock) = jest.fn().mockReturnValue(mockLogger)
jest.resetModules()
const { withTiming } = require('./logger')
const mockOperation = jest.fn().mockResolvedValue('success')
const result = await withTiming('test-operation', mockOperation, { test: 'metadata' })
expect(result).toBe('success')
expect(mockLogger.debug).toHaveBeenCalledTimes(2) // Start and complete
expect(mockLogger.debug).toHaveBeenCalledWith(
expect.stringContaining('Started test-operation'),
{ test: 'metadata' }
)
})
it('should handle timing operation failures', async () => {
// Setup mocks
const mockLogger = {
debug: jest.fn(),
error: jest.fn()
}
;(winston.createLogger as jest.Mock) = jest.fn().mockReturnValue(mockLogger)
jest.resetModules()
const { withTiming } = require('./logger')
const mockOperation = jest.fn().mockRejectedValue(new Error('test error'))
await expect(withTiming('test-operation', mockOperation)).rejects.toThrow('test error')
expect(mockLogger.debug).toHaveBeenCalledWith(
expect.stringContaining('Started test-operation'),
{}
)
expect(mockLogger.error).toHaveBeenCalledWith(
expect.stringContaining('Failed test-operation'),
expect.objectContaining({
duration: expect.any(Number),
error: expect.any(Error)
})
)
})
})
+120
View File
@@ -0,0 +1,120 @@
import winston from 'winston'
import LokiTransport from 'winston-loki'
const isDevelopment = process.env.NODE_ENV === 'development'
// Custom format for structured logging
const structuredFormat = winston.format.combine(
winston.format.timestamp({ format: 'ISO' }),
winston.format.errors({ stack: true }),
winston.format.json({
replacer: (_key, value) =>
typeof value === 'bigint' ? value.toString() : value,
})
)
// Console format for development
const consoleFormat = winston.format.combine(
winston.format.timestamp({ format: 'HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.colorize(),
winston.format.simple(),
winston.format.printf(({ timestamp, level, message, service, requestId, ...meta }) => {
const requestInfo = requestId ? `[${requestId}]` : ''
const serviceInfo = service ? `[${service}]` : '[mozdIT]'
const metaStr = Object.keys(meta).length ? `\n${JSON.stringify(meta, null, 2)}` : ''
return `${timestamp} ${serviceInfo} ${level} ${requestInfo} ${message}${metaStr}`
})
)
// Transports configuration
const transports: winston.transport[] = [
// Loki transport for centralized logging
...(process.env.LOKI_HOST
? [
new LokiTransport({
host: process.env.LOKI_HOST,
labels: {
app: 'mozdit-web',
environment: process.env.NODE_ENV || 'development',
service: 'frontend'
},
basicAuth: process.env.LOKI_USERNAME && process.env.LOKI_PASSWORD
? `${process.env.LOKI_USERNAME}:${process.env.LOKI_PASSWORD}`
: undefined,
json: true,
format: winston.format.json(),
onConnectionError: (err: Error) => console.error('Loki connection error:', err)
})
]
: []),
// Console for development logging
new winston.transports.Console({
level: isDevelopment ? 'debug' : 'info',
format: isDevelopment ? consoleFormat : structuredFormat,
handleExceptions: true,
handleRejections: true
})
]
// Root logger configuration
export const logger = winston.createLogger({
level: isDevelopment ? 'debug' : 'info',
format: structuredFormat,
defaultMeta: {
service: 'mozdit-web',
version: process.env.npm_package_version || '1.0.0',
environment: process.env.NODE_ENV || 'development'
},
transports,
exceptionHandlers: transports,
rejectionHandlers: transports
})
// Specialized loggers for different components
export const createComponentLogger = (component: string) => {
return logger.child({ component })
}
export const requestLogger = logger.child({ component: 'request' })
export const apiLogger = logger.child({ component: 'api' })
export const dbLogger = logger.child({ component: 'database' })
export const authLogger = logger.child({ component: 'auth' })
export const errorLogger = logger.child({ component: 'error' })
// Request ID generator for correlation
export const generateRequestId = (): string =>
`${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
// Helper function for timing operations
export const withTiming = async <T>(
operation: string,
fn: () => Promise<T>,
metadata: any = {}
): Promise<T> => {
const startTime = Date.now()
logger.debug(`Started ${operation}`, metadata)
try {
const result = await fn()
const duration = Date.now() - startTime
logger.debug(`Completed ${operation} in ${duration}ms`, { ...metadata, duration })
return result
} catch (error) {
const duration = Date.now() - startTime
logger.error(`Failed ${operation} in ${duration}ms`, { ...metadata, duration, error })
throw error
}
}
// Graceful shutdown
const gracefulShutdown = () => {
logger.info('Initiating graceful shutdown...')
logger.end()
}
process.on('SIGTERM', gracefulShutdown)
process.on('SIGINT', gracefulShutdown)
export default logger
+154
View File
@@ -0,0 +1,154 @@
import {
getCollection,
getDb,
checkMongoConnection
} from './mongodb'
import { MongoClient } from 'mongodb'
const mockMongoClient = MongoClient as jest.MockedClass<typeof MongoClient>
// Mock the MongoDB client and database
const mockDb = {
collection: jest.fn().mockReturnValue({
findOne: jest.fn(),
insertOne: jest.fn(),
updateOne: jest.fn(),
deleteOne: jest.fn()
})
}
const mockClient = {
connect: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
db: jest.fn().mockReturnValue(mockDb)
}
// Mock the mongodb module
jest.mock('mongodb', () => ({
MongoClient: jest.fn().mockImplementation(() => mockClient)
}))
// Restore environment before tests
const originalEnv = process.env
describe('MongoDB Connection', () => {
beforeEach(() => {
process.env = {
...originalEnv,
MONGODB_URI: 'mongodb://localhost:27017/test',
MONGODB_DB: 'test'
}
jest.clearAllMocks()
})
afterEach(() => {
process.env = originalEnv
})
describe('MongoDB URI validation', () => {
it('should throw error when MONGODB_URI is not set', () => {
// This test is tricky because the module is cached
// We'll test this by clearing the module cache and mocking process.env
const originalMongodbUri = process.env.MONGODB_URI
delete process.env.MONGODB_URI
// Clear module cache to force re-import
jest.resetModules()
expect(() => {
require('./mongodb')
}).toThrow('Please add MONGODB_URI to your environment variables')
// Restore environment
process.env.MONGODB_URI = originalMongodbUri
})
})
describe('Database connection', () => {
it('should create MongoClient with correct URI and options', async () => {
const { MongoClient } = require('mongodb')
// Reset modules to use our mock
jest.resetModules()
const { clientPromise } = require('./mongodb')
await clientPromise
expect(MongoClient).toHaveBeenCalledWith(
process.env.MONGODB_URI,
expect.objectContaining({
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000
})
)
})
it('should return database instance', async () => {
// Reset modules to use our mock
jest.resetModules()
const { getDb } = require('./mongodb')
const result = await getDb()
expect(result).toBeDefined()
expect(typeof result.collection).toBe('function')
})
it('should return collection from database', async () => {
// Reset modules to use our mock
jest.resetModules()
const { getCollection } = require('./mongodb')
const collection = await getCollection('test_collection')
expect(collection).toBeDefined()
expect(typeof collection.findOne).toBe('function')
})
it('should check MongoDB connection successfully', async () => {
// Reset modules to use our mock
jest.resetModules()
const { checkMongoConnection } = require('./mongodb')
const result = await checkMongoConnection()
expect(result).toBe(true)
})
it('should return false on MongoDB connection failure', async () => {
// Mock a connection failure
const { MongoClient } = require('mongodb')
// Create a failing client
const failingClient = {
connect: jest.fn().mockRejectedValue(new Error('Connection failed')),
db: jest.fn(),
close: jest.fn()
}
MongoClient.mockImplementation(() => failingClient)
// Reset the module to use the new mock
jest.resetModules()
const { checkMongoConnection } = require('./mongodb')
const result = await checkMongoConnection()
expect(result).toBe(false)
})
})
describe('Connection reuse and caching', () => {
it('should reuse the same MongoClient instance for multiple calls', async () => {
// Reset modules to use our mock
jest.resetModules()
const { clientPromise: clientPromise1 } = require('./mongodb')
const { clientPromise: clientPromise2 } = require('./mongodb')
await clientPromise1
await clientPromise2
// Should still be the same promise from cache
expect(clientPromise1).toBe(clientPromise2)
})
})
})
+55
View File
@@ -0,0 +1,55 @@
import { MongoClient, Db } from 'mongodb'
if (!process.env.MONGODB_URI) {
throw new Error('Please add MONGODB_URI to your environment variables')
}
const uri = process.env.MONGODB_URI
const options = {
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
}
let client: MongoClient
let clientPromise: Promise<MongoClient>
// In development mode, use a global variable so that the client is not recreated between hot reloads
if (process.env.NODE_ENV === 'development') {
// @ts-ignore
if (!global._mongoClientPromise) {
client = new MongoClient(uri, options)
// @ts-ignore
global._mongoClientPromise = client.connect()
}
// @ts-ignore
clientPromise = global._mongoClientPromise
} else {
// In production mode, it's best to not use a global variable
client = new MongoClient(uri, options)
clientPromise = client.connect()
}
export default clientPromise
export async function getDb(): Promise<Db> {
const client = await clientPromise
return client.db(process.env.MONGODB_DB || 'mozdit')
}
export async function getCollection(collectionName: string) {
const db = await getDb()
return db.collection(collectionName)
}
// Health check for MongoDB connection
export async function checkMongoConnection(): Promise<boolean> {
try {
const db = await getDb()
await db.admin().ping()
return true
} catch (error) {
console.error('MongoDB connection check failed:', error)
return false
}
}
+382
View File
@@ -0,0 +1,382 @@
import { getSiteConfig, saveSiteConfig, initializeDefaultConfig } from './site-config'
import { getCollection } from './mongodb'
import { siteConfig as staticConfig } from '@/config/site'
import { SiteConfig } from '@/types/site'
import logger from './logger'
// Mock dependencies
jest.mock('./mongodb')
jest.mock('./logger', () => ({
__esModule: true,
default: {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn()
}
}))
jest.mock('@/config/site', () => ({
siteConfig: {
general: {
name: 'Test Site',
description: 'Test Description',
url: 'https://test.com',
ogImage: 'https://test.com/og.jpg',
locale: 'en'
},
navigation: {
main: [
{ label: 'Home', href: '/' },
{ label: 'About', href: '/about' }
],
footer: [
{ label: 'Privacy', href: '/privacy' },
{ label: 'Terms', href: '/terms' }
]
},
hero: {
title: 'Welcome',
subtitle: 'Test Subtitle',
description: 'Test description',
cta: {
primary: {
text: 'Get Started',
href: '/get-started'
}
}
},
services: {
title: 'Our Services',
subtitle: 'What we offer',
services: []
},
about: {
title: 'About Us',
description: ['Test about section'],
usps: []
},
footer: {
copyright: '© 2024 Test Site',
links: []
},
contact: {
email: 'test@test.com',
address: 'Test Address',
form: {
title: 'Contact Us',
description: 'Get in touch',
submitText: 'Send Message',
fields: {
name: { label: 'Name', placeholder: 'Your name', required: true },
email: { label: 'Email', placeholder: 'your@email.com', required: true },
message: { label: 'Message', placeholder: 'Your message', required: true },
consent: { label: 'I agree', required: true }
}
}
}
}
}))
const mockCollection = {
findOne: jest.fn(),
replaceOne: jest.fn()
}
describe('Site Config', () => {
beforeEach(() => {
jest.clearAllMocks()
;(getCollection as jest.Mock).mockResolvedValue(mockCollection)
})
describe('getSiteConfig', () => {
it('should return MongoDB config when available', async () => {
const mockConfig: SiteConfig = {
general: {
name: 'MongoDB Site',
description: 'MongoDB Description',
url: 'https://mongodb.com',
ogImage: 'https://mongodb.com/og.jpg',
locale: 'en'
},
navigation: {
main: [{ label: 'Home', href: '/' }],
footer: [{ label: 'Privacy', href: '/privacy' }]
},
hero: {
title: 'MongoDB Hero',
subtitle: 'MongoDB Subtitle',
description: 'MongoDB Description',
cta: {
primary: {
text: 'Get Started',
href: '/get-started'
}
}
},
services: {
title: 'Services',
subtitle: 'Our services',
services: []
},
about: {
title: 'About',
description: ['About us'],
usps: []
},
footer: {
copyright: '© 2024 MongoDB',
links: []
},
contact: {
email: 'mongodb@test.com',
address: 'MongoDB Address',
form: {
title: 'Contact',
description: 'Get in touch',
submitText: 'Send',
fields: {
name: { label: 'Name', placeholder: 'Name', required: true },
email: { label: 'Email', placeholder: 'Email', required: true },
message: { label: 'Message', placeholder: 'Message', required: true },
consent: { label: 'Consent', required: true }
}
}
}
}
mockCollection.findOne.mockResolvedValue({
_id: 'mock-id',
data: mockConfig
})
const config = await getSiteConfig()
expect(config).toEqual(mockConfig)
expect(mockCollection.findOne).toHaveBeenCalledWith({
type: 'site_config',
environment: 'development'
})
expect(logger.info).toHaveBeenCalledWith(
'Loaded site config from MongoDB',
{ configId: 'mock-id' }
)
})
it('should return static config when MongoDB config not found', async () => {
mockCollection.findOne.mockResolvedValue(null)
const config = await getSiteConfig()
expect(config).toEqual(staticConfig)
expect(logger.info).toHaveBeenCalledWith(
'MongoDB config not found, using static fallback'
)
})
it('should return static config when MongoDB is unavailable', async () => {
mockCollection.findOne.mockRejectedValue(new Error('Connection failed'))
const config = await getSiteConfig()
expect(config).toEqual(staticConfig)
expect(logger.warn).toHaveBeenCalledWith(
'MongoDB unavailable, using static config',
{ error: 'Connection failed' }
)
})
})
describe('saveSiteConfig', () => {
it('should save config to MongoDB successfully', async () => {
const newConfig: SiteConfig = {
general: {
name: 'New Site',
description: 'New Description',
url: 'https://new.com',
ogImage: 'https://new.com/og.jpg',
locale: 'en'
},
navigation: {
main: [{ label: 'Home', href: '/' }],
footer: [{ label: 'Privacy', href: '/privacy' }]
},
hero: {
title: 'New Hero',
subtitle: 'New Subtitle',
description: 'New Description',
cta: {
primary: {
text: 'Get Started',
href: '/get-started'
}
}
},
services: {
title: 'Services',
subtitle: 'Our services',
services: []
},
about: {
title: 'About',
description: ['About us'],
usps: []
},
footer: {
copyright: '© 2024 New',
links: []
},
contact: {
email: 'new@test.com',
address: 'New Address',
form: {
title: 'Contact',
description: 'Get in touch',
submitText: 'Send',
fields: {
name: { label: 'Name', placeholder: 'Name', required: true },
email: { label: 'Email', placeholder: 'Email', required: true },
message: { label: 'Message', placeholder: 'Message', required: true },
consent: { label: 'Consent', required: true }
}
}
}
}
mockCollection.replaceOne.mockResolvedValue({ acknowledged: true })
const result = await saveSiteConfig(newConfig)
expect(result).toBe(true)
expect(mockCollection.replaceOne).toHaveBeenCalledWith(
{ type: 'site_config', environment: 'development' },
{
type: 'site_config',
environment: 'development',
data: newConfig,
lastModified: expect.any(Date)
},
{ upsert: true }
)
expect(logger.info).toHaveBeenCalledWith('Saved site config to MongoDB')
})
it('should return false when save fails', async () => {
const newConfig: SiteConfig = {
general: {
name: 'New Site',
description: 'New Description',
url: 'https://new.com',
ogImage: 'https://new.com/og.jpg',
locale: 'en'
},
navigation: {
main: [{ label: 'Home', href: '/' }],
footer: [{ label: 'Privacy', href: '/privacy' }]
},
hero: {
title: 'New Hero',
subtitle: 'New Subtitle',
description: 'New Description',
cta: {
primary: {
text: 'Get Started',
href: '/get-started'
}
}
},
services: {
title: 'Services',
subtitle: 'Our services',
services: []
},
about: {
title: 'About',
description: ['About us'],
usps: []
},
footer: {
copyright: '© 2024 New',
links: []
},
contact: {
email: 'new@test.com',
address: 'New Address',
form: {
title: 'Contact',
description: 'Get in touch',
submitText: 'Send',
fields: {
name: { label: 'Name', placeholder: 'Name', required: true },
email: { label: 'Email', placeholder: 'Email', required: true },
message: { label: 'Message', placeholder: 'Message', required: true },
consent: { label: 'Consent', required: true }
}
}
}
}
mockCollection.replaceOne.mockRejectedValue(new Error('Save failed'))
const result = await saveSiteConfig(newConfig)
expect(result).toBe(false)
expect(logger.error).toHaveBeenCalledWith(
'Failed to save config to MongoDB',
{ error: 'Save failed' }
)
})
})
describe('initializeDefaultConfig', () => {
it('should initialize default config when none exists', async () => {
mockCollection.findOne.mockResolvedValue(null)
mockCollection.replaceOne.mockResolvedValue({ acknowledged: true })
await initializeDefaultConfig()
expect(mockCollection.findOne).toHaveBeenCalledWith({
type: 'site_config',
environment: 'development'
})
expect(mockCollection.replaceOne).toHaveBeenCalledWith(
{ type: 'site_config', environment: 'development' },
{
type: 'site_config',
environment: 'development',
data: staticConfig,
lastModified: expect.any(Date)
},
{ upsert: true }
)
expect(logger.info).toHaveBeenCalledWith(
'Initialized default site config in MongoDB'
)
})
it('should not initialize when config already exists', async () => {
mockCollection.findOne.mockResolvedValue({
_id: 'existing-id',
data: staticConfig
})
await initializeDefaultConfig()
expect(mockCollection.replaceOne).not.toHaveBeenCalled()
expect(logger.info).toHaveBeenCalledWith(
'Site config already exists in MongoDB'
)
})
it('should handle initialization errors gracefully', async () => {
mockCollection.findOne.mockRejectedValue(new Error('Connection failed'))
await initializeDefaultConfig()
expect(logger.error).toHaveBeenCalledWith(
'Failed to initialize site config',
{ error: 'Connection failed' }
)
})
})
})
+74
View File
@@ -0,0 +1,74 @@
// Hybrid approach: File-based fallback + MongoDB integration for future
import { siteConfig as staticConfig } from '@/config/site'
import { SiteConfig } from '@/types/site'
import { getCollection } from './mongodb'
import logger from './logger'
/**
* Get site configuration with hybrid approach
* 1. Try MongoDB first (production-ready)
* 2. Fall back to static file (development/development safe)
*/
export async function getSiteConfig(): Promise<SiteConfig> {
try {
const collection = await getCollection('site_config')
const doc = await collection.findOne({ type: 'site_config', environment: 'development' })
if (doc && doc.data) {
logger.info('Loaded site config from MongoDB', { configId: doc._id })
return doc.data as SiteConfig
} else {
logger.info('MongoDB config not found, using static fallback')
return staticConfig
}
} catch (error) {
logger.warn('MongoDB unavailable, using static config', { error: (error as Error).message })
return staticConfig
}
}
/**
* Save site configuration to MongoDB
* For future admin panel integration
*/
export async function saveSiteConfig(config: SiteConfig): Promise<boolean> {
try {
const collection = await getCollection('site_config')
await collection.replaceOne(
{ type: 'site_config', environment: 'development' },
{
type: 'site_config',
environment: 'development',
data: config,
lastModified: new Date()
},
{ upsert: true }
)
logger.info('Saved site config to MongoDB')
return true
} catch (error) {
logger.error('Failed to save config to MongoDB', { error: (error as Error).message })
return false
}
}
/**
* Initialize default config in MongoDB (one-time setup)
*/
export async function initializeDefaultConfig(): Promise<void> {
try {
const collection = await getCollection('site_config')
const existingConfig = await collection.findOne({ type: 'site_config', environment: 'development' })
if (!existingConfig) {
await saveSiteConfig(staticConfig)
logger.info('Initialized default site config in MongoDB')
} else {
logger.info('Site config already exists in MongoDB')
}
} catch (error) {
logger.error('Failed to initialize site config', { error: (error as Error).message })
}
}
+129
View File
@@ -0,0 +1,129 @@
export interface SiteConfig {
general: GeneralConfig
navigation: NavigationConfig
hero: HeroSectionConfig
services: ServiceSectionConfig
about: AboutSectionConfig
footer: FooterConfig
contact: ContactConfig
}
export interface GeneralConfig {
name: string
description: string
url: string
ogImage: string
locale: string
}
export interface NavigationItem {
label: string
href: string
external?: boolean
}
export interface NavigationConfig {
main: NavigationItem[]
footer: NavigationItem[]
}
export interface HeroSectionConfig {
title: string
subtitle: string
description: string
cta: {
primary: {
text: string
href: string
external?: boolean
}
secondary?: {
text: string
href: string
}
}
}
export interface ServiceItem {
id: string
title: string
description: string
icon: string
features: string[]
ctaText: string
}
export interface ServiceSectionConfig {
title: string
subtitle: string
services: ServiceItem[]
}
export interface USP {
id: string
title: string
description: string
icon: string
}
export interface AboutSectionConfig {
title: string
description: string[]
usps: USP[]
}
export interface FooterConfig {
copyright: string
links: {
label: string
href: string
}[]
}
export interface ContactConfig {
phone?: string
email: string
address: string
socialMedia?: {
platform: string
url: string
label: string
}[]
form: {
title: string
description: string
submitText: string
fields: {
name: {
label: string
placeholder: string
required: boolean
}
email: {
label: string
placeholder: string
required: boolean
}
message: {
label: string
placeholder: string
required: boolean
}
consent: {
label: string
required: boolean
}
}
}
}
export interface EnvConfig {
siteUrl: string
companyName: string
contactEmail: string
webmailUrl?: string
analytics: {
plausibleDomain?: string
ga4Id?: string
}
}