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 = {
|
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}`,
|
title: `${siteConfig.general.name} | ${siteConfig.general.description}`,
|
||||||
description: siteConfig.general.description,
|
description: siteConfig.general.description,
|
||||||
authors: [{ name: siteConfig.general.name }],
|
authors: [{ name: siteConfig.general.name }],
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { render, screen, fireEvent } from '@testing-library/react'
|
|||||||
import '@testing-library/jest-dom'
|
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'
|
||||||
|
|
||||||
// Mock Next.js Link component
|
// Mock Next.js Link component
|
||||||
jest.mock('next/link', () => {
|
jest.mock('next/link', () => {
|
||||||
@@ -52,14 +53,14 @@ describe('Header', () => {
|
|||||||
|
|
||||||
// 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
|
||||||
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()
|
expect(hamburgerButton).toBeInTheDocument()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should have proper accessibility attributes', () => {
|
it('should have proper accessibility attributes', () => {
|
||||||
render(<Header />)
|
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')
|
expect(hamburgerButton).toHaveAttribute('aria-expanded', 'false')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -82,7 +83,7 @@ describe('Header', () => {
|
|||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
render(<Header />)
|
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
|
// Initially menu should be closed
|
||||||
expect(hamburgerButton).toHaveAttribute('aria-expanded', 'false')
|
expect(hamburgerButton).toHaveAttribute('aria-expanded', 'false')
|
||||||
@@ -100,7 +101,7 @@ describe('Header', () => {
|
|||||||
const user = userEvent.setup()
|
const user = userEvent.setup()
|
||||||
render(<Header />)
|
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
|
// Open the mobile menu
|
||||||
await user.click(hamburgerButton)
|
await user.click(hamburgerButton)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'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'
|
||||||
|
|
||||||
@@ -66,15 +67,15 @@ export default function Header() {
|
|||||||
target={item.external ? '_blank' : undefined}
|
target={item.external ? '_blank' : undefined}
|
||||||
rel={item.external ? 'noopener noreferrer' : undefined}
|
rel={item.external ? 'noopener noreferrer' : undefined}
|
||||||
className={`relative px-4 py-2 rounded-lg text-sm font-medium transition-all duration-200 ${
|
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'
|
? 'btn btn-primary text-white'
|
||||||
: 'hover-scale'
|
: 'hover-scale'
|
||||||
}`}
|
}`}
|
||||||
style={item.label !== 'Kapcsolat' ? {
|
style={!item.primary ? {
|
||||||
color: 'var(--color-foreground)',
|
color: 'var(--color-foreground)',
|
||||||
} : undefined}
|
} : undefined}
|
||||||
>
|
>
|
||||||
{item.label !== 'Kapcsolat' && (
|
{!item.primary && (
|
||||||
<span
|
<span
|
||||||
className="absolute inset-0 rounded-lg opacity-0 hover:opacity-100 transition-opacity duration-200"
|
className="absolute inset-0 rounded-lg opacity-0 hover:opacity-100 transition-opacity duration-200"
|
||||||
style={{ background: 'var(--color-primary-50)' }}
|
style={{ background: 'var(--color-primary-50)' }}
|
||||||
@@ -110,7 +111,7 @@ export default function Header() {
|
|||||||
}}
|
}}
|
||||||
aria-expanded={isMenuOpen}
|
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">
|
<div className="relative w-6 h-6">
|
||||||
{/* Hamburger to X animation */}
|
{/* Hamburger to X animation */}
|
||||||
<span
|
<span
|
||||||
@@ -154,23 +155,23 @@ export default function Header() {
|
|||||||
rel={item.external ? 'noopener noreferrer' : undefined}
|
rel={item.external ? 'noopener noreferrer' : undefined}
|
||||||
onClick={() => setIsMenuOpen(false)}
|
onClick={() => setIsMenuOpen(false)}
|
||||||
className={`block px-4 py-3 rounded-lg text-base font-medium transition-all duration-200 ${
|
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={{
|
style={{
|
||||||
animationDelay: `${index * 50}ms`,
|
animationDelay: `${index * 50}ms`,
|
||||||
...(item.label !== 'Kapcsolat' ? {
|
...(!item.primary ? {
|
||||||
color: 'var(--color-foreground)',
|
color: 'var(--color-foreground)',
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
} : {})
|
} : {})
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (item.label !== 'Kapcsolat') {
|
if (!item.primary) {
|
||||||
e.currentTarget.style.background = 'var(--color-primary-50)'
|
e.currentTarget.style.background = 'var(--color-primary-50)'
|
||||||
e.currentTarget.style.color = 'var(--color-primary-600)'
|
e.currentTarget.style.color = 'var(--color-primary-600)'
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
if (item.label !== 'Kapcsolat') {
|
if (!item.primary) {
|
||||||
e.currentTarget.style.background = 'transparent'
|
e.currentTarget.style.background = 'transparent'
|
||||||
e.currentTarget.style.color = 'var(--color-foreground)'
|
e.currentTarget.style.color = 'var(--color-foreground)'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const siteConfig: SiteConfig = {
|
|||||||
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.',
|
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: '/og-image.png',
|
ogImage: '/mozdit_logo_text.png',
|
||||||
locale: 'hu-HU'
|
locale: 'hu-HU'
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export const siteConfig: SiteConfig = {
|
|||||||
{ label: 'Kezdőlap', href: '/' },
|
{ label: 'Kezdőlap', href: '/' },
|
||||||
{ label: 'Rólunk', href: '/rolunk' },
|
{ label: 'Rólunk', href: '/rolunk' },
|
||||||
{ label: 'Szolgáltatások', href: '/szolgaltatasok' },
|
{ label: 'Szolgáltatások', href: '/szolgaltatasok' },
|
||||||
{ label: 'Kapcsolat', href: '/kapcsolat' }
|
{ label: 'Kapcsolat', href: '/kapcsolat', primary: true }
|
||||||
],
|
],
|
||||||
footer: [
|
footer: [
|
||||||
{ label: 'Kezdőlap', href: '/' },
|
{ label: 'Kezdőlap', href: '/' },
|
||||||
@@ -41,32 +41,7 @@ export const siteConfig: SiteConfig = {
|
|||||||
|
|
||||||
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 content/common.json (footer.address) — CMS-editable.
|
||||||
form: {
|
// The form fields live in content/pages/contact.json.
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,5 +20,9 @@
|
|||||||
"footer": {
|
"footer": {
|
||||||
"copyright": "© 2002–{year} mozdIT Bt. Minden jog fenntartva.",
|
"copyright": "© 2002–{year} mozdIT Bt. Minden jog fenntartva.",
|
||||||
"address": "Szigetszentmiklós, Magyarország"
|
"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 }),
|
validation: object({ required: string, invalidEmail: string, minLength: string }),
|
||||||
staging: object({ banner: string }),
|
staging: object({ banner: string }),
|
||||||
footer: object({ copyright: string, address: string }),
|
footer: object({ copyright: string, address: string }),
|
||||||
|
a11y: object({ openMenu: string, closeMenu: string }),
|
||||||
}),
|
}),
|
||||||
home: object({
|
home: object({
|
||||||
hero: object({ title: string, subtitle: string, description: string, trustBullets: array(string), cta: object({ primary: ctaLink, secondary: ctaLink }) }),
|
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
|
copyright: string
|
||||||
address: string
|
address: string
|
||||||
}
|
}
|
||||||
|
a11y: {
|
||||||
|
openMenu: string
|
||||||
|
closeMenu: string
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LegalPageContent {
|
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
|
label: string
|
||||||
href: string
|
href: string
|
||||||
external?: boolean
|
external?: boolean
|
||||||
|
primary?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NavigationConfig {
|
export interface NavigationConfig {
|
||||||
@@ -83,38 +84,11 @@ export interface FooterConfig {
|
|||||||
export interface ContactConfig {
|
export interface ContactConfig {
|
||||||
phone?: string
|
phone?: string
|
||||||
email: string
|
email: string
|
||||||
address: string
|
|
||||||
socialMedia?: {
|
socialMedia?: {
|
||||||
platform: string
|
platform: string
|
||||||
url: string
|
url: string
|
||||||
label: 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 {
|
export interface EnvConfig {
|
||||||
|
|||||||
Reference in New Issue
Block a user