feat: footer cert marks + social icons
This commit is contained in:
parent
14a383abd2
commit
cab4779f7d
@ -1,9 +1,12 @@
|
|||||||
import { getCachedGlobal } from '@/utilities/getGlobals'
|
import { getCachedGlobal } from '@/utilities/getGlobals'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import type { Footer, Page } from '@/payload-types'
|
import type { Footer, Media, Page } from '@/payload-types'
|
||||||
import { CMSLink } from '@/components/Link'
|
import { CMSLink } from '@/components/Link'
|
||||||
import { Logo } from '@/components/Logo/Logo'
|
import { Logo } from '@/components/Logo/Logo'
|
||||||
|
import { FDImage } from '@/components/FDImage'
|
||||||
|
import { SocialIconsRow } from '@/components/SocialIcons'
|
||||||
|
import type { SocialLinkData } from '@/components/SocialIcons'
|
||||||
|
|
||||||
/** Resolves the logo href from the logoLink group field */
|
/** Resolves the logo href from the logoLink group field */
|
||||||
function resolveLogoHref(logoLink: Footer['logoLink']): string {
|
function resolveLogoHref(logoLink: Footer['logoLink']): string {
|
||||||
@ -19,68 +22,130 @@ function resolveLogoHref(logoLink: Footer['logoLink']): string {
|
|||||||
return logoLink.url || '/'
|
return logoLink.url || '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Builds a flat, ordered list of enabled social links from the socialLinks group */
|
||||||
|
export function buildSocialLinks(socialLinks: Footer['socialLinks']): SocialLinkData[] {
|
||||||
|
if (!socialLinks) return []
|
||||||
|
const result: SocialLinkData[] = []
|
||||||
|
if (socialLinks.linkedinEnabled && socialLinks.linkedinUrl)
|
||||||
|
result.push({ platform: 'linkedin', url: socialLinks.linkedinUrl, enabled: true })
|
||||||
|
if (socialLinks.instagramEnabled && socialLinks.instagramUrl)
|
||||||
|
result.push({ platform: 'instagram', url: socialLinks.instagramUrl, enabled: true })
|
||||||
|
if (socialLinks.facebookEnabled && socialLinks.facebookUrl)
|
||||||
|
result.push({ platform: 'facebook', url: socialLinks.facebookUrl, enabled: true })
|
||||||
|
if (socialLinks.youtubeEnabled && socialLinks.youtubeUrl)
|
||||||
|
result.push({ platform: 'youtube', url: socialLinks.youtubeUrl, enabled: true })
|
||||||
|
if (socialLinks.twitterEnabled && socialLinks.twitterUrl)
|
||||||
|
result.push({ platform: 'twitter', url: socialLinks.twitterUrl, enabled: true })
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
export async function Footer() {
|
export async function Footer() {
|
||||||
const footerData = await getCachedGlobal('footer', 1)() as unknown as Footer
|
const footerData = await getCachedGlobal('footer', 1)() as unknown as Footer
|
||||||
const columns = footerData?.columns || []
|
const columns = footerData?.columns || []
|
||||||
const navItems = footerData?.navItems || []
|
const navItems = footerData?.navItems || []
|
||||||
|
const certMarks = footerData?.certMarks || []
|
||||||
const hasColumns = columns.length > 0
|
const hasColumns = columns.length > 0
|
||||||
|
const socialLinks = buildSocialLinks(footerData?.socialLinks)
|
||||||
|
|
||||||
const logoHref = resolveLogoHref(footerData?.logoLink)
|
const logoHref = resolveLogoHref(footerData?.logoLink)
|
||||||
const bottomLeft = (footerData?.bottomLeftText || '© {year} Fiber Direkt. Alla rättigheter förbehållna.').replace('{year}', new Date().getFullYear().toString())
|
const bottomLeft = (
|
||||||
const bottomRight = footerData?.bottomRightText || 'Svenskt datacenter · ISO 27001 · ISO 14001'
|
footerData?.bottomLeftText || '© {year} Fiber Direkt. Alla rättigheter förbehållna.'
|
||||||
|
).replace('{year}', new Date().getFullYear().toString())
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<footer className="mt-auto bg-fd-navy text-white">
|
<footer className="mt-auto bg-fd-navy text-white">
|
||||||
<div className="max-w-[1200px] mx-auto px-6 md:px-8 py-12 md:py-16 flex flex-col gap-10">
|
<div className="max-w-[1200px] mx-auto px-6 md:px-8 py-12 md:py-16 flex flex-col gap-10">
|
||||||
{/* Top section: Logo + columns or simple nav */}
|
|
||||||
<div className="flex flex-col lg:flex-row gap-10 lg:gap-16">
|
|
||||||
{/* Logo column */}
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<Link className="flex items-center" href={logoHref}>
|
|
||||||
<Logo variant="white" className="max-w-[120px]" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Sitemap columns */}
|
{/* ── Top bar: Logo (left) + Cert marks (right) ───────────────────── */}
|
||||||
{hasColumns ? (
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-6">
|
||||||
<div className="flex-1 grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8 md:gap-10">
|
<Link className="flex items-center flex-shrink-0" href={logoHref}>
|
||||||
{columns.map((column, colIndex) => (
|
<Logo variant="white" className="max-w-[120px]" />
|
||||||
<div key={colIndex} className="flex flex-col gap-4">
|
</Link>
|
||||||
<span className="font-joey-bold text-fd-yellow text-base md:text-lg">
|
|
||||||
{column.heading}
|
{certMarks.length > 0 && (
|
||||||
|
<div className="flex items-center gap-4 flex-wrap">
|
||||||
|
{certMarks.map((mark, i) => {
|
||||||
|
const media = mark.image as Media | null | undefined
|
||||||
|
if (!media) return null
|
||||||
|
const imgEl = (
|
||||||
|
<FDImage
|
||||||
|
media={media}
|
||||||
|
size="thumbnail"
|
||||||
|
className="h-12 w-auto object-contain"
|
||||||
|
fallbackAlt={mark.alt || ''}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
return mark.linkUrl ? (
|
||||||
|
<a
|
||||||
|
key={i}
|
||||||
|
href={mark.linkUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="opacity-80 hover:opacity-100 transition-opacity"
|
||||||
|
>
|
||||||
|
{imgEl}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span key={i} className="opacity-80">
|
||||||
|
{imgEl}
|
||||||
</span>
|
</span>
|
||||||
<nav className="flex flex-col gap-2.5">
|
)
|
||||||
{column.links?.map(({ link }, linkIndex) => (
|
})}
|
||||||
<CMSLink
|
|
||||||
className="font-joey text-white/70 hover:text-white text-sm md:text-base transition-colors no-underline"
|
|
||||||
key={linkIndex}
|
|
||||||
{...link}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<nav className="flex flex-col md:flex-row md:items-center gap-4 md:ml-auto">
|
|
||||||
{navItems.map(({ link }, i) => (
|
|
||||||
<CMSLink
|
|
||||||
className="font-joey text-white hover:text-fd-yellow transition-colors no-underline"
|
|
||||||
key={i}
|
|
||||||
{...link}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</nav>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* ── Sitemap columns ──────────────────────────────────────────────── */}
|
||||||
|
{hasColumns && (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8 md:gap-10">
|
||||||
|
{columns.map((column, colIndex) => (
|
||||||
|
<div key={colIndex} className="flex flex-col gap-4">
|
||||||
|
<span className="font-joey-bold text-fd-yellow text-base md:text-lg">
|
||||||
|
{column.heading}
|
||||||
|
</span>
|
||||||
|
<nav className="flex flex-col gap-2.5">
|
||||||
|
{column.links?.map(({ link }, linkIndex) => (
|
||||||
|
<CMSLink
|
||||||
|
className="font-joey text-white/70 hover:text-white text-sm md:text-base transition-colors no-underline"
|
||||||
|
key={linkIndex}
|
||||||
|
{...link}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* ── Divider ──────────────────────────────────────────────────────── */}
|
||||||
<hr className="border-white/20" />
|
<hr className="border-white/20" />
|
||||||
|
|
||||||
{/* Bottom row */}
|
{/* ── Bottom row: copyright · nav links · social icons ────────────── */}
|
||||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<p className="font-joey text-white/60 text-sm">{bottomLeft}</p>
|
|
||||||
<p className="font-joey text-white/60 text-sm">{bottomRight}</p>
|
{/* Left + center: copyright text and legal nav links */}
|
||||||
|
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-6 flex-wrap">
|
||||||
|
<p className="font-joey text-white/60 text-sm whitespace-nowrap">{bottomLeft}</p>
|
||||||
|
{navItems.length > 0 && (
|
||||||
|
<nav className="flex items-center gap-4 flex-wrap">
|
||||||
|
{navItems.map(({ link }, i) => (
|
||||||
|
<CMSLink
|
||||||
|
className="font-joey text-white/60 hover:text-white text-sm transition-colors no-underline underline-offset-2 hover:underline"
|
||||||
|
key={i}
|
||||||
|
{...link}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: social media icons */}
|
||||||
|
<SocialIconsRow
|
||||||
|
links={socialLinks}
|
||||||
|
linkClassName="text-white/60 hover:text-white"
|
||||||
|
iconSize="w-5 h-5"
|
||||||
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@ -50,6 +50,134 @@ export const Footer: GlobalConfig = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Certifieringsmärken ───────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
name: 'certMarks',
|
||||||
|
type: 'array',
|
||||||
|
label: 'Certifieringsmärken',
|
||||||
|
maxRows: 6,
|
||||||
|
admin: {
|
||||||
|
description: 'Logotyper och certifieringsbadges som visas i övre högra hörnet av footern (t.ex. ISO, UC-sigill, Based in Sweden).',
|
||||||
|
initCollapsed: true,
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'image',
|
||||||
|
type: 'upload',
|
||||||
|
label: 'Bild',
|
||||||
|
relationTo: 'media',
|
||||||
|
required: true,
|
||||||
|
admin: {
|
||||||
|
description: 'PNG eller SVG, helst med transparent bakgrund.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'alt',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Alt-text',
|
||||||
|
admin: {
|
||||||
|
description: 'Beskriv certifieringen för tillgänglighet, t.ex. "ISO 27001 certifierad".',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'linkUrl',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Länk (valfri)',
|
||||||
|
admin: {
|
||||||
|
description: 'Länk till certifieringens webbsida (öppnas i ny flik).',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Sociala medier ────────────────────────────────────────────────────────
|
||||||
|
{
|
||||||
|
name: 'socialLinks',
|
||||||
|
type: 'group',
|
||||||
|
label: 'Sociala medier',
|
||||||
|
admin: {
|
||||||
|
description: 'Ikoner visas längst ner i footern och i mobilmenyn. Aktivera och ange URL för varje plattform.',
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
// LinkedIn
|
||||||
|
{
|
||||||
|
name: 'linkedinEnabled',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Visa LinkedIn',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'linkedinUrl',
|
||||||
|
type: 'text',
|
||||||
|
label: 'LinkedIn URL',
|
||||||
|
admin: {
|
||||||
|
condition: (_, siblingData) => Boolean(siblingData?.linkedinEnabled),
|
||||||
|
description: 'T.ex. https://www.linkedin.com/company/fiber-direkt',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Instagram
|
||||||
|
{
|
||||||
|
name: 'instagramEnabled',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Visa Instagram',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'instagramUrl',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Instagram URL',
|
||||||
|
admin: {
|
||||||
|
condition: (_, siblingData) => Boolean(siblingData?.instagramEnabled),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Facebook
|
||||||
|
{
|
||||||
|
name: 'facebookEnabled',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Visa Facebook',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'facebookUrl',
|
||||||
|
type: 'text',
|
||||||
|
label: 'Facebook URL',
|
||||||
|
admin: {
|
||||||
|
condition: (_, siblingData) => Boolean(siblingData?.facebookEnabled),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// YouTube
|
||||||
|
{
|
||||||
|
name: 'youtubeEnabled',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Visa YouTube',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'youtubeUrl',
|
||||||
|
type: 'text',
|
||||||
|
label: 'YouTube URL',
|
||||||
|
admin: {
|
||||||
|
condition: (_, siblingData) => Boolean(siblingData?.youtubeEnabled),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Twitter / X
|
||||||
|
{
|
||||||
|
name: 'twitterEnabled',
|
||||||
|
type: 'checkbox',
|
||||||
|
label: 'Visa X (Twitter)',
|
||||||
|
defaultValue: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'twitterUrl',
|
||||||
|
type: 'text',
|
||||||
|
label: 'X (Twitter) URL',
|
||||||
|
admin: {
|
||||||
|
condition: (_, siblingData) => Boolean(siblingData?.twitterEnabled),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
// ── Sitemap columns ───────────────────────────────────────────────────────
|
// ── Sitemap columns ───────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
name: 'columns',
|
name: 'columns',
|
||||||
@ -91,13 +219,13 @@ export const Footer: GlobalConfig = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Simple nav (legacy) ───────────────────────────────────────────────────
|
// ── Simple nav (legacy / bottom links) ────────────────────────────────────
|
||||||
{
|
{
|
||||||
name: 'navItems',
|
name: 'navItems',
|
||||||
type: 'array',
|
type: 'array',
|
||||||
label: 'Enkel navigering (äldre)',
|
label: 'Nedre navigering',
|
||||||
admin: {
|
admin: {
|
||||||
description: 'Enkla footer-länkar (visas om inga kolumner finns)',
|
description: 'Länkar i underfältet (visas som rad bredvid copyright, t.ex. Policys · Legal · Villkor)',
|
||||||
initCollapsed: true,
|
initCollapsed: true,
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
@ -105,25 +233,19 @@ export const Footer: GlobalConfig = {
|
|||||||
appearances: false,
|
appearances: false,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
maxRows: 6,
|
maxRows: 8,
|
||||||
},
|
},
|
||||||
|
|
||||||
// ── Bottom text ───────────────────────────────────────────────────────────
|
// ── Bottom text ───────────────────────────────────────────────────────────
|
||||||
{
|
{
|
||||||
name: 'bottomLeftText',
|
name: 'bottomLeftText',
|
||||||
type: 'text',
|
type: 'text',
|
||||||
label: 'Nedre vänster text',
|
label: 'Copyrighttext (nedre vänster)',
|
||||||
defaultValue: '© {year} Fiber Direkt. Alla rättigheter förbehållna.',
|
defaultValue: '© {year} Fiber Direkt. Alla rättigheter förbehållna.',
|
||||||
admin: {
|
admin: {
|
||||||
description: 'Använd {year} för aktuellt årtal',
|
description: 'Använd {year} för aktuellt årtal',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: 'bottomRightText',
|
|
||||||
type: 'text',
|
|
||||||
label: 'Nedre höger text',
|
|
||||||
defaultValue: 'Svenskt datacenter · ISO 27001 · ISO 14001',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
hooks: {
|
hooks: {
|
||||||
afterChange: [revalidateFooter],
|
afterChange: [revalidateFooter],
|
||||||
|
|||||||
@ -5,12 +5,14 @@ import { usePathname } from 'next/navigation'
|
|||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
|
|
||||||
import type { Header, Page } from '@/payload-types'
|
import type { Header, Page } from '@/payload-types'
|
||||||
|
import type { SocialLinkData } from '@/components/SocialIcons'
|
||||||
|
|
||||||
import { Logo } from '@/components/Logo/Logo'
|
import { Logo } from '@/components/Logo/Logo'
|
||||||
import { HeaderNav } from './Nav'
|
import { HeaderNav } from './Nav'
|
||||||
|
|
||||||
interface HeaderClientProps {
|
interface HeaderClientProps {
|
||||||
data: Header
|
data: Header
|
||||||
|
socialLinks?: SocialLinkData[]
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveLogoHref(logoLink: Header['logoLink']): string {
|
function resolveLogoHref(logoLink: Header['logoLink']): string {
|
||||||
@ -26,7 +28,7 @@ function resolveLogoHref(logoLink: Header['logoLink']): string {
|
|||||||
return logoLink.url || '/'
|
return logoLink.url || '/'
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
|
export const HeaderClient: React.FC<HeaderClientProps> = ({ data, socialLinks = [] }) => {
|
||||||
const [theme, setTheme] = useState<string | null>(null)
|
const [theme, setTheme] = useState<string | null>(null)
|
||||||
const [isDark, setIsDark] = useState(false)
|
const [isDark, setIsDark] = useState(false)
|
||||||
const { headerTheme, setHeaderTheme } = useHeaderTheme()
|
const { headerTheme, setHeaderTheme } = useHeaderTheme()
|
||||||
@ -59,9 +61,9 @@ export const HeaderClient: React.FC<HeaderClientProps> = ({ data }) => {
|
|||||||
>
|
>
|
||||||
<div className="container py-5 flex justify-between items-center">
|
<div className="container py-5 flex justify-between items-center">
|
||||||
<Link href={logoHref}>
|
<Link href={logoHref}>
|
||||||
<Logo loading="eager" priority="high" variant={isDark ? "white" : "blue"} className="max-w-[80px] md:max-w-[100px]" />
|
<Logo loading="eager" priority="high" variant={isDark ? 'white' : 'blue'} className="max-w-[80px] md:max-w-[100px]" />
|
||||||
</Link>
|
</Link>
|
||||||
<HeaderNav data={data} />
|
<HeaderNav data={data} socialLinks={socialLinks} />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -2,10 +2,15 @@ import { HeaderClient } from './Component.client'
|
|||||||
import { getCachedGlobal } from '@/utilities/getGlobals'
|
import { getCachedGlobal } from '@/utilities/getGlobals'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
import type { Header } from '@/payload-types'
|
import type { Header, Footer } from '@/payload-types'
|
||||||
|
import { buildSocialLinks } from '@/Footer/Component'
|
||||||
|
|
||||||
export async function Header() {
|
export async function Header() {
|
||||||
const headerData = await getCachedGlobal("header", 1)() as unknown as Header
|
const headerData = await getCachedGlobal('header', 1)() as unknown as Header
|
||||||
|
|
||||||
return <HeaderClient data={headerData} />
|
// Also fetch footer social links so the mobile menu can show them
|
||||||
|
const footerData = await getCachedGlobal('footer', 1)() as unknown as Footer
|
||||||
|
const socialLinks = buildSocialLinks(footerData?.socialLinks)
|
||||||
|
|
||||||
|
return <HeaderClient data={headerData} socialLinks={socialLinks} />
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import type { Header as HeaderType, Page } from '@/payload-types'
|
|||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { MenuIcon, XIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react'
|
import { MenuIcon, XIcon, ChevronDownIcon, ChevronRightIcon } from 'lucide-react'
|
||||||
|
import { SocialIconsRow } from '@/components/SocialIcons'
|
||||||
|
import type { SocialLinkData } from '@/components/SocialIcons'
|
||||||
|
|
||||||
type NavChild = NonNullable<NonNullable<NonNullable<HeaderType['navItems']>[number]['children']>[number]>
|
type NavChild = NonNullable<NonNullable<NonNullable<HeaderType['navItems']>[number]['children']>[number]>
|
||||||
type NavItem = NonNullable<HeaderType['navItems']>[number]
|
type NavItem = NonNullable<HeaderType['navItems']>[number]
|
||||||
@ -142,7 +144,10 @@ const MegaMenuPanel: React.FC<{
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
export const HeaderNav: React.FC<{ data: HeaderType; socialLinks?: SocialLinkData[] }> = ({
|
||||||
|
data,
|
||||||
|
socialLinks = [],
|
||||||
|
}) => {
|
||||||
const navItems = (data?.navItems || []) as NavItem[]
|
const navItems = (data?.navItems || []) as NavItem[]
|
||||||
const [mobileOpen, setMobileOpen] = useState(false)
|
const [mobileOpen, setMobileOpen] = useState(false)
|
||||||
const [openDropdown, setOpenDropdown] = useState<string | null>(null)
|
const [openDropdown, setOpenDropdown] = useState<string | null>(null)
|
||||||
@ -280,7 +285,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
|||||||
{mobileOpen ? <XIcon className="w-6 h-6" /> : <MenuIcon className="w-6 h-6" />}
|
{mobileOpen ? <XIcon className="w-6 h-6" /> : <MenuIcon className="w-6 h-6" />}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* ── Mobile full-screen overlay — always navy, no dark: needed ── */}
|
{/* ── Mobile full-screen overlay ── */}
|
||||||
<div
|
<div
|
||||||
className={`fixed inset-0 z-50 bg-fd-navy flex flex-col transition-transform duration-300 md:hidden ${
|
className={`fixed inset-0 z-50 bg-fd-navy flex flex-col transition-transform duration-300 md:hidden ${
|
||||||
mobileOpen ? 'translate-x-0' : 'translate-x-full'
|
mobileOpen ? 'translate-x-0' : 'translate-x-full'
|
||||||
@ -289,6 +294,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Navigeringsmeny"
|
aria-label="Navigeringsmeny"
|
||||||
>
|
>
|
||||||
|
{/* Header bar */}
|
||||||
<div className="flex items-center justify-between px-6 py-5 border-b border-white/10">
|
<div className="flex items-center justify-between px-6 py-5 border-b border-white/10">
|
||||||
<Link href="/" onClick={closeMobile}>
|
<Link href="/" onClick={closeMobile}>
|
||||||
<span className="font-joey-heavy text-fd-yellow text-xl">Fiber Direkt</span>
|
<span className="font-joey-heavy text-fd-yellow text-xl">Fiber Direkt</span>
|
||||||
@ -302,6 +308,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Nav links */}
|
||||||
<nav className="flex-1 overflow-y-auto px-6 py-6 flex flex-col">
|
<nav className="flex-1 overflow-y-auto px-6 py-6 flex flex-col">
|
||||||
{navItems.map((item, i) => {
|
{navItems.map((item, i) => {
|
||||||
const hasChildren = item.children && item.children.length > 0
|
const hasChildren = item.children && item.children.length > 0
|
||||||
@ -347,6 +354,17 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
|||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
{/* ── Social icons row at bottom of mobile menu ── */}
|
||||||
|
{socialLinks.length > 0 && (
|
||||||
|
<div className="px-6 py-5 border-t border-white/10">
|
||||||
|
<SocialIconsRow
|
||||||
|
links={socialLinks}
|
||||||
|
linkClassName="text-white/50 hover:text-white"
|
||||||
|
iconSize="w-6 h-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,8 +9,8 @@ export const metadata: Metadata = {
|
|||||||
|
|
||||||
export default function NotFound() {
|
export default function NotFound() {
|
||||||
return (
|
return (
|
||||||
<main className="min-h-[calc(100vh-80px)] bg-white dark:bg-fd-navy flex items-center overflow-hidden">
|
<main className="w-full bg-white dark:bg-fd-navy lg:min-h-[calc(100vh-80px)] lg:flex lg:items-center overflow-hidden">
|
||||||
<div className="max-w-[1200px] mx-auto px-6 md:px-8 w-full pt-8 pb-16 md:pt-12 md:pb-20">
|
<div className="max-w-[1200px] mx-auto px-6 md:px-8 w-full py-16 md:py-20 lg:py-[99px]">
|
||||||
<div className="flex flex-col lg:flex-row items-center gap-12 lg:gap-20">
|
<div className="flex flex-col lg:flex-row items-center gap-12 lg:gap-20">
|
||||||
|
|
||||||
{/* ── Left: Text content ─────────────────────────────────────── */}
|
{/* ── Left: Text content ─────────────────────────────────────── */}
|
||||||
@ -48,10 +48,9 @@ export default function NotFound() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Right: Modem SVG illustration ──────────────────────────── */}
|
{/* ── Right: Modem SVG illustration — desktop only ───────────── */}
|
||||||
<div className="hidden lg:flex flex-1 items-center justify-center w-full max-w-[380px] lg:max-w-none">
|
<div className="hidden lg:flex flex-1 items-center justify-center w-full max-w-[380px] lg:max-w-none">
|
||||||
<div className="relative w-full max-w-[460px]">
|
<div className="relative w-full max-w-[460px]">
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 1536 1024"
|
viewBox="0 0 1536 1024"
|
||||||
fill="none"
|
fill="none"
|
||||||
@ -67,7 +66,6 @@ export default function NotFound() {
|
|||||||
className="dark:fill-white"
|
className="dark:fill-white"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -59,24 +59,29 @@ export const AnnouncementBarComponent: React.FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`w-full px-4 py-2 flex items-center justify-center gap-4 text-sm font-joey relative ${bgClass}`}
|
className={`w-full px-10 py-2 relative flex items-center justify-center ${bgClass}`}
|
||||||
role="status"
|
role="status"
|
||||||
>
|
>
|
||||||
<span>{text}</span>
|
{/* Centered content — px-10 leaves room for the dismiss button on both sides */}
|
||||||
{buttonLabel && href && (
|
<div className="flex items-center justify-center gap-3 text-sm font-joey text-center flex-wrap">
|
||||||
<a
|
<span>{text}</span>
|
||||||
href={href}
|
{buttonLabel && href && (
|
||||||
target={newTab ? '_blank' : undefined}
|
<a
|
||||||
rel={newTab ? 'noopener noreferrer' : undefined}
|
href={href}
|
||||||
className="underline font-joey-bold hover:opacity-70 transition-opacity"
|
target={newTab ? '_blank' : undefined}
|
||||||
>
|
rel={newTab ? 'noopener noreferrer' : undefined}
|
||||||
{buttonLabel}
|
className="underline font-joey-bold hover:opacity-70 transition-opacity whitespace-nowrap"
|
||||||
</a>
|
>
|
||||||
)}
|
{buttonLabel}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dismiss — truly absolute so it doesn't affect centering */}
|
||||||
{dismissible && (
|
{dismissible && (
|
||||||
<button
|
<button
|
||||||
onClick={handleDismiss}
|
onClick={handleDismiss}
|
||||||
className="absolute right-4 opacity-60 hover:opacity-100 transition-opacity"
|
className="absolute right-4 top-1/2 -translate-y-1/2 opacity-60 hover:opacity-100 transition-opacity"
|
||||||
aria-label="Stäng notis"
|
aria-label="Stäng notis"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
|
|||||||
106
src/components/SocialIcons/index.tsx
Normal file
106
src/components/SocialIcons/index.tsx
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface IconProps {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LinkedInIcon: React.FC<IconProps> = ({ className }) => (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} aria-hidden>
|
||||||
|
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z" fill="currentColor" />
|
||||||
|
<rect x="2" y="9" width="4" height="12" fill="currentColor" />
|
||||||
|
<circle cx="4" cy="4" r="2" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const InstagramIcon: React.FC<IconProps> = ({ className }) => (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} aria-hidden>
|
||||||
|
<rect x="2" y="2" width="20" height="20" rx="5.5" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
<circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||||
|
<circle cx="17.5" cy="6.5" r="1" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const FacebookIcon: React.FC<IconProps> = ({ className }) => (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} aria-hidden>
|
||||||
|
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const YouTubeIcon: React.FC<IconProps> = ({ className }) => (
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} aria-hidden>
|
||||||
|
<rect x="1" y="4" width="22" height="16" rx="4" stroke="currentColor" strokeWidth="2" />
|
||||||
|
<polygon points="9.5,8.5 9.5,15.5 16.5,12" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const XTwitterIcon: React.FC<IconProps> = ({ className }) => (
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className={className} aria-hidden>
|
||||||
|
<path d="M13.8097 10.2033L22.448 0H20.401L12.9004 8.85942L6.90962 0H0L9.05919 13.397L0 24.0968H2.04712L9.96801 14.7409L16.2947 24.0968H23.2043L13.8092 10.2033H13.8097ZM11.0059 13.515L10.088 12.181L2.78473 1.5659H5.92899L11.8228 10.1326L12.7407 11.4667L20.402 22.6021H17.2577L11.0059 13.5155V13.515Z" fill="currentColor" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
|
||||||
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type SocialPlatform = 'linkedin' | 'instagram' | 'facebook' | 'youtube' | 'twitter'
|
||||||
|
|
||||||
|
const PLATFORM_ICONS: Record<SocialPlatform, React.FC<IconProps>> = {
|
||||||
|
linkedin: LinkedInIcon,
|
||||||
|
instagram: InstagramIcon,
|
||||||
|
facebook: FacebookIcon,
|
||||||
|
youtube: YouTubeIcon,
|
||||||
|
twitter: XTwitterIcon,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PLATFORM_LABELS: Record<SocialPlatform, string> = {
|
||||||
|
linkedin: 'LinkedIn',
|
||||||
|
instagram: 'Instagram',
|
||||||
|
facebook: 'Facebook',
|
||||||
|
youtube: 'YouTube',
|
||||||
|
twitter: 'X (Twitter)',
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── SocialIconsRow ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface SocialLinkData {
|
||||||
|
platform: SocialPlatform
|
||||||
|
url: string
|
||||||
|
enabled?: boolean | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SocialIconsRowProps {
|
||||||
|
links: SocialLinkData[]
|
||||||
|
className?: string
|
||||||
|
/** Classes on each <a> — controls color via currentColor */
|
||||||
|
linkClassName?: string
|
||||||
|
iconSize?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SocialIconsRow: React.FC<SocialIconsRowProps> = ({
|
||||||
|
links,
|
||||||
|
className = '',
|
||||||
|
linkClassName = 'text-white/60 hover:text-white',
|
||||||
|
iconSize = 'w-5 h-5',
|
||||||
|
}) => {
|
||||||
|
const activeLinks = links.filter((l) => l.enabled && l.url)
|
||||||
|
if (activeLinks.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center gap-4 ${className}`}>
|
||||||
|
{activeLinks.map(({ platform, url }) => {
|
||||||
|
const Icon = PLATFORM_ICONS[platform]
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
key={platform}
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
aria-label={PLATFORM_LABELS[platform]}
|
||||||
|
className={`transition-colors duration-200 ${linkClassName}`}
|
||||||
|
>
|
||||||
|
<Icon className={iconSize} />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -2780,6 +2780,44 @@ export interface Footer {
|
|||||||
*/
|
*/
|
||||||
url?: string | null;
|
url?: string | null;
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* Logotyper och certifieringsbadges som visas i övre högra hörnet av footern (t.ex. ISO, UC-sigill, Based in Sweden).
|
||||||
|
*/
|
||||||
|
certMarks?:
|
||||||
|
| {
|
||||||
|
/**
|
||||||
|
* PNG eller SVG, helst med transparent bakgrund.
|
||||||
|
*/
|
||||||
|
image: number | Media;
|
||||||
|
/**
|
||||||
|
* Beskriv certifieringen för tillgänglighet, t.ex. "ISO 27001 certifierad".
|
||||||
|
*/
|
||||||
|
alt?: string | null;
|
||||||
|
/**
|
||||||
|
* Länk till certifieringens webbsida (öppnas i ny flik).
|
||||||
|
*/
|
||||||
|
linkUrl?: string | null;
|
||||||
|
id?: string | null;
|
||||||
|
}[]
|
||||||
|
| null;
|
||||||
|
/**
|
||||||
|
* Ikoner visas längst ner i footern och i mobilmenyn. Aktivera och ange URL för varje plattform.
|
||||||
|
*/
|
||||||
|
socialLinks?: {
|
||||||
|
linkedinEnabled?: boolean | null;
|
||||||
|
/**
|
||||||
|
* T.ex. https://www.linkedin.com/company/fiber-direkt
|
||||||
|
*/
|
||||||
|
linkedinUrl?: string | null;
|
||||||
|
instagramEnabled?: boolean | null;
|
||||||
|
instagramUrl?: string | null;
|
||||||
|
facebookEnabled?: boolean | null;
|
||||||
|
facebookUrl?: string | null;
|
||||||
|
youtubeEnabled?: boolean | null;
|
||||||
|
youtubeUrl?: string | null;
|
||||||
|
twitterEnabled?: boolean | null;
|
||||||
|
twitterUrl?: string | null;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Footer-kolumner med rubriker och länkar (sitemap-stil)
|
* Footer-kolumner med rubriker och länkar (sitemap-stil)
|
||||||
*/
|
*/
|
||||||
@ -2813,7 +2851,7 @@ export interface Footer {
|
|||||||
}[]
|
}[]
|
||||||
| null;
|
| null;
|
||||||
/**
|
/**
|
||||||
* Enkla footer-länkar (visas om inga kolumner finns)
|
* Länkar i underfältet (visas som rad bredvid copyright, t.ex. Policys · Legal · Villkor)
|
||||||
*/
|
*/
|
||||||
navItems?:
|
navItems?:
|
||||||
| {
|
| {
|
||||||
@ -2839,7 +2877,6 @@ export interface Footer {
|
|||||||
* Använd {year} för aktuellt årtal
|
* Använd {year} för aktuellt årtal
|
||||||
*/
|
*/
|
||||||
bottomLeftText?: string | null;
|
bottomLeftText?: string | null;
|
||||||
bottomRightText?: string | null;
|
|
||||||
updatedAt?: string | null;
|
updatedAt?: string | null;
|
||||||
createdAt?: string | null;
|
createdAt?: string | null;
|
||||||
}
|
}
|
||||||
@ -3014,6 +3051,28 @@ export interface FooterSelect<T extends boolean = true> {
|
|||||||
reference?: T;
|
reference?: T;
|
||||||
url?: T;
|
url?: T;
|
||||||
};
|
};
|
||||||
|
certMarks?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
image?: T;
|
||||||
|
alt?: T;
|
||||||
|
linkUrl?: T;
|
||||||
|
id?: T;
|
||||||
|
};
|
||||||
|
socialLinks?:
|
||||||
|
| T
|
||||||
|
| {
|
||||||
|
linkedinEnabled?: T;
|
||||||
|
linkedinUrl?: T;
|
||||||
|
instagramEnabled?: T;
|
||||||
|
instagramUrl?: T;
|
||||||
|
facebookEnabled?: T;
|
||||||
|
facebookUrl?: T;
|
||||||
|
youtubeEnabled?: T;
|
||||||
|
youtubeUrl?: T;
|
||||||
|
twitterEnabled?: T;
|
||||||
|
twitterUrl?: T;
|
||||||
|
};
|
||||||
columns?:
|
columns?:
|
||||||
| T
|
| T
|
||||||
| {
|
| {
|
||||||
@ -3049,7 +3108,6 @@ export interface FooterSelect<T extends boolean = true> {
|
|||||||
id?: T;
|
id?: T;
|
||||||
};
|
};
|
||||||
bottomLeftText?: T;
|
bottomLeftText?: T;
|
||||||
bottomRightText?: T;
|
|
||||||
updatedAt?: T;
|
updatedAt?: T;
|
||||||
createdAt?: T;
|
createdAt?: T;
|
||||||
globalType?: T;
|
globalType?: T;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user