fix(web): hygiene — OG image/metadataBase, Header a11y text + nav flag, dead code
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
- siteConfig.ogImage pointed to an existing asset; metadataBase added so relative OG/Twitter URLs resolve against the real origin instead of localhost - Header sr-only menu label moved to common.json (a11y.openMenu/closeMenu); navigation primary styling driven by a NavigationItem.primary flag instead of brittle 'Kapcsolat' string comparisons - remove dead code: lib/site-config.ts (unused hybrid config) and the unused, schema-divergent siteConfig.contact.form - ContactConfig type cleaned up (address/form removed) Closes MITHOME-77
This commit is contained in:
@@ -19,6 +19,9 @@ const geistMono = Geist_Mono({
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
// WHY metadataBase: without it Next resolves relative OG/twitter image URLs
|
||||
// against localhost, producing broken social previews in production.
|
||||
metadataBase: new URL(siteConfig.general.url),
|
||||
title: `${siteConfig.general.name} | ${siteConfig.general.description}`,
|
||||
description: siteConfig.general.description,
|
||||
authors: [{ name: siteConfig.general.name }],
|
||||
|
||||
@@ -2,6 +2,7 @@ import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import '@testing-library/jest-dom'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import Header from './Header'
|
||||
import { common } from '@/content'
|
||||
|
||||
// Mock Next.js Link component
|
||||
jest.mock('next/link', () => {
|
||||
@@ -52,14 +53,14 @@ describe('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', { name: /open main menu/i })
|
||||
const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') })
|
||||
expect(hamburgerButton).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should have proper accessibility attributes', () => {
|
||||
render(<Header />)
|
||||
|
||||
const hamburgerButton = screen.getByRole('button', { name: /open main menu/i })
|
||||
const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') })
|
||||
expect(hamburgerButton).toHaveAttribute('aria-expanded', 'false')
|
||||
})
|
||||
|
||||
@@ -82,7 +83,7 @@ describe('Header', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<Header />)
|
||||
|
||||
const hamburgerButton = screen.getByRole('button', { name: /open main menu/i })
|
||||
const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') })
|
||||
|
||||
// Initially menu should be closed
|
||||
expect(hamburgerButton).toHaveAttribute('aria-expanded', 'false')
|
||||
@@ -100,7 +101,7 @@ describe('Header', () => {
|
||||
const user = userEvent.setup()
|
||||
render(<Header />)
|
||||
|
||||
const hamburgerButton = screen.getByRole('button', { name: /open main menu/i })
|
||||
const hamburgerButton = screen.getByRole('button', { name: new RegExp(common.a11y.openMenu, 'i') })
|
||||
|
||||
// Open the mobile menu
|
||||
await user.click(hamburgerButton)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { siteConfig } from '@/config/site'
|
||||
import { common } from '@/content'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ThemeToggle } from './ThemeProvider'
|
||||
|
||||
@@ -66,15 +67,15 @@ export default function Header() {
|
||||
target={item.external ? '_blank' : undefined}
|
||||
rel={item.external ? 'noopener noreferrer' : undefined}
|
||||
className={`relative px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
||||
item.label === 'Kapcsolat'
|
||||
item.primary
|
||||
? 'btn btn-primary text-white'
|
||||
: 'hover-scale'
|
||||
}`}
|
||||
style={item.label !== 'Kapcsolat' ? {
|
||||
style={!item.primary ? {
|
||||
color: 'var(--color-foreground)',
|
||||
} : undefined}
|
||||
>
|
||||
{item.label !== 'Kapcsolat' && (
|
||||
{!item.primary && (
|
||||
<span
|
||||
className="absolute inset-0 rounded-lg opacity-0 hover:opacity-100 transition-opacity duration-200"
|
||||
style={{ background: 'var(--color-primary-50)' }}
|
||||
@@ -110,7 +111,7 @@ export default function Header() {
|
||||
}}
|
||||
aria-expanded={isMenuOpen}
|
||||
>
|
||||
<span className="sr-only">{isMenuOpen ? 'Close main menu' : 'Open main menu'}</span>
|
||||
<span className="sr-only">{isMenuOpen ? common.a11y.closeMenu : common.a11y.openMenu}</span>
|
||||
<div className="relative w-6 h-6">
|
||||
{/* Hamburger to X animation */}
|
||||
<span
|
||||
@@ -154,23 +155,23 @@ export default function Header() {
|
||||
rel={item.external ? 'noopener noreferrer' : undefined}
|
||||
onClick={() => setIsMenuOpen(false)}
|
||||
className={`block px-4 py-3 rounded-lg text-base font-medium transition-all duration-200 ${
|
||||
item.label === 'Kapcsolat' ? 'btn btn-primary text-white mt-2' : ''
|
||||
item.primary ? 'btn btn-primary text-white mt-2' : ''
|
||||
}`}
|
||||
style={{
|
||||
animationDelay: `${index * 50}ms`,
|
||||
...(item.label !== 'Kapcsolat' ? {
|
||||
...(!item.primary ? {
|
||||
color: 'var(--color-foreground)',
|
||||
background: 'transparent',
|
||||
} : {})
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
if (item.label !== 'Kapcsolat') {
|
||||
if (!item.primary) {
|
||||
e.currentTarget.style.background = 'var(--color-primary-50)'
|
||||
e.currentTarget.style.color = 'var(--color-primary-600)'
|
||||
}
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
if (item.label !== 'Kapcsolat') {
|
||||
if (!item.primary) {
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
e.currentTarget.style.color = 'var(--color-foreground)'
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export const siteConfig: SiteConfig = {
|
||||
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',
|
||||
ogImage: '/mozdit_logo_text.png',
|
||||
locale: 'hu-HU'
|
||||
},
|
||||
|
||||
@@ -19,7 +19,7 @@ export const siteConfig: SiteConfig = {
|
||||
{ label: 'Kezdőlap', href: '/' },
|
||||
{ label: 'Rólunk', href: '/rolunk' },
|
||||
{ label: 'Szolgáltatások', href: '/szolgaltatasok' },
|
||||
{ label: 'Kapcsolat', href: '/kapcsolat' }
|
||||
{ label: 'Kapcsolat', href: '/kapcsolat', primary: true }
|
||||
],
|
||||
footer: [
|
||||
{ label: 'Kezdőlap', href: '/' },
|
||||
@@ -41,32 +41,7 @@ export const siteConfig: SiteConfig = {
|
||||
|
||||
contact: {
|
||||
email: process.env.NEXT_PUBLIC_CONTACT_EMAIL || 'info@mozdit.hu',
|
||||
// address lives in content/common.json (footer.address) — CMS-editable
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
// address lives in content/common.json (footer.address) — CMS-editable.
|
||||
// The form fields live in content/pages/contact.json.
|
||||
}
|
||||
}
|
||||
@@ -20,5 +20,9 @@
|
||||
"footer": {
|
||||
"copyright": "© 2002–{year} mozdIT Bt. Minden jog fenntartva.",
|
||||
"address": "Szigetszentmiklós, Magyarország"
|
||||
},
|
||||
"a11y": {
|
||||
"openMenu": "Főmenü megnyitása",
|
||||
"closeMenu": "Főmenü bezárása"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ const schemas = {
|
||||
validation: object({ required: string, invalidEmail: string, minLength: string }),
|
||||
staging: object({ banner: string }),
|
||||
footer: object({ copyright: string, address: string }),
|
||||
a11y: object({ openMenu: string, closeMenu: string }),
|
||||
}),
|
||||
home: object({
|
||||
hero: object({ title: string, subtitle: string, description: string, trustBullets: array(string), cta: object({ primary: ctaLink, secondary: ctaLink }) }),
|
||||
|
||||
@@ -197,6 +197,10 @@ export interface CommonContent {
|
||||
copyright: string
|
||||
address: string
|
||||
}
|
||||
a11y: {
|
||||
openMenu: string
|
||||
closeMenu: string
|
||||
}
|
||||
}
|
||||
|
||||
export interface LegalPageContent {
|
||||
|
||||
@@ -1,382 +0,0 @@
|
||||
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' }
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,74 +0,0 @@
|
||||
// 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 })
|
||||
}
|
||||
}
|
||||
+1
-27
@@ -20,6 +20,7 @@ export interface NavigationItem {
|
||||
label: string
|
||||
href: string
|
||||
external?: boolean
|
||||
primary?: boolean
|
||||
}
|
||||
|
||||
export interface NavigationConfig {
|
||||
@@ -83,38 +84,11 @@ export interface FooterConfig {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user