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 Link from 'next/link'
|
||||
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 { 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 */
|
||||
function resolveLogoHref(logoLink: Footer['logoLink']): string {
|
||||
@ -19,68 +22,130 @@ function resolveLogoHref(logoLink: Footer['logoLink']): string {
|
||||
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() {
|
||||
const footerData = await getCachedGlobal('footer', 1)() as unknown as Footer
|
||||
const columns = footerData?.columns || []
|
||||
const navItems = footerData?.navItems || []
|
||||
const certMarks = footerData?.certMarks || []
|
||||
const hasColumns = columns.length > 0
|
||||
const socialLinks = buildSocialLinks(footerData?.socialLinks)
|
||||
|
||||
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 bottomRight = footerData?.bottomRightText || 'Svenskt datacenter · ISO 27001 · ISO 14001'
|
||||
const bottomLeft = (
|
||||
footerData?.bottomLeftText || '© {year} Fiber Direkt. Alla rättigheter förbehållna.'
|
||||
).replace('{year}', new Date().getFullYear().toString())
|
||||
|
||||
return (
|
||||
<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">
|
||||
{/* 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 */}
|
||||
{hasColumns ? (
|
||||
<div className="flex-1 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}
|
||||
{/* ── Top bar: Logo (left) + Cert marks (right) ───────────────────── */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-6">
|
||||
<Link className="flex items-center flex-shrink-0" href={logoHref}>
|
||||
<Logo variant="white" className="max-w-[120px]" />
|
||||
</Link>
|
||||
|
||||
{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>
|
||||
<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>
|
||||
) : (
|
||||
<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>
|
||||
|
||||
{/* 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" />
|
||||
|
||||
{/* Bottom row */}
|
||||
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
|
||||
<p className="font-joey text-white/60 text-sm">{bottomLeft}</p>
|
||||
<p className="font-joey text-white/60 text-sm">{bottomRight}</p>
|
||||
{/* ── Bottom row: copyright · nav links · social icons ────────────── */}
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
|
||||
{/* 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>
|
||||
</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 ───────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'columns',
|
||||
@ -91,13 +219,13 @@ export const Footer: GlobalConfig = {
|
||||
],
|
||||
},
|
||||
|
||||
// ── Simple nav (legacy) ───────────────────────────────────────────────────
|
||||
// ── Simple nav (legacy / bottom links) ────────────────────────────────────
|
||||
{
|
||||
name: 'navItems',
|
||||
type: 'array',
|
||||
label: 'Enkel navigering (äldre)',
|
||||
label: 'Nedre navigering',
|
||||
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,
|
||||
},
|
||||
fields: [
|
||||
@ -105,25 +233,19 @@ export const Footer: GlobalConfig = {
|
||||
appearances: false,
|
||||
}),
|
||||
],
|
||||
maxRows: 6,
|
||||
maxRows: 8,
|
||||
},
|
||||
|
||||
// ── Bottom text ───────────────────────────────────────────────────────────
|
||||
{
|
||||
name: 'bottomLeftText',
|
||||
type: 'text',
|
||||
label: 'Nedre vänster text',
|
||||
label: 'Copyrighttext (nedre vänster)',
|
||||
defaultValue: '© {year} Fiber Direkt. Alla rättigheter förbehållna.',
|
||||
admin: {
|
||||
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: {
|
||||
afterChange: [revalidateFooter],
|
||||
|
||||
@ -5,12 +5,14 @@ import { usePathname } from 'next/navigation'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
|
||||
import type { Header, Page } from '@/payload-types'
|
||||
import type { SocialLinkData } from '@/components/SocialIcons'
|
||||
|
||||
import { Logo } from '@/components/Logo/Logo'
|
||||
import { HeaderNav } from './Nav'
|
||||
|
||||
interface HeaderClientProps {
|
||||
data: Header
|
||||
socialLinks?: SocialLinkData[]
|
||||
}
|
||||
|
||||
function resolveLogoHref(logoLink: Header['logoLink']): string {
|
||||
@ -26,7 +28,7 @@ function resolveLogoHref(logoLink: Header['logoLink']): string {
|
||||
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 [isDark, setIsDark] = useState(false)
|
||||
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">
|
||||
<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>
|
||||
<HeaderNav data={data} />
|
||||
<HeaderNav data={data} socialLinks={socialLinks} />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
@ -2,10 +2,15 @@ import { HeaderClient } from './Component.client'
|
||||
import { getCachedGlobal } from '@/utilities/getGlobals'
|
||||
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() {
|
||||
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 { usePathname } from 'next/navigation'
|
||||
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 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 [mobileOpen, setMobileOpen] = useState(false)
|
||||
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" />}
|
||||
</button>
|
||||
|
||||
{/* ── Mobile full-screen overlay — always navy, no dark: needed ── */}
|
||||
{/* ── Mobile full-screen overlay ── */}
|
||||
<div
|
||||
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'
|
||||
@ -289,6 +294,7 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
||||
aria-modal="true"
|
||||
aria-label="Navigeringsmeny"
|
||||
>
|
||||
{/* Header bar */}
|
||||
<div className="flex items-center justify-between px-6 py-5 border-b border-white/10">
|
||||
<Link href="/" onClick={closeMobile}>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* Nav links */}
|
||||
<nav className="flex-1 overflow-y-auto px-6 py-6 flex flex-col">
|
||||
{navItems.map((item, i) => {
|
||||
const hasChildren = item.children && item.children.length > 0
|
||||
@ -347,6 +354,17 @@ export const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {
|
||||
)
|
||||
})}
|
||||
</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>
|
||||
</>
|
||||
)
|
||||
|
||||
@ -9,8 +9,8 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function NotFound() {
|
||||
return (
|
||||
<main className="min-h-[calc(100vh-80px)] bg-white dark:bg-fd-navy flex 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">
|
||||
<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 py-16 md:py-20 lg:py-[99px]">
|
||||
<div className="flex flex-col lg:flex-row items-center gap-12 lg:gap-20">
|
||||
|
||||
{/* ── Left: Text content ─────────────────────────────────────── */}
|
||||
@ -48,10 +48,9 @@ export default function NotFound() {
|
||||
</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="relative w-full max-w-[460px]">
|
||||
|
||||
<svg
|
||||
viewBox="0 0 1536 1024"
|
||||
fill="none"
|
||||
@ -67,7 +66,6 @@ export default function NotFound() {
|
||||
className="dark:fill-white"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -59,24 +59,29 @@ export const AnnouncementBarComponent: React.FC<Props> = ({
|
||||
|
||||
return (
|
||||
<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"
|
||||
>
|
||||
<span>{text}</span>
|
||||
{buttonLabel && href && (
|
||||
<a
|
||||
href={href}
|
||||
target={newTab ? '_blank' : undefined}
|
||||
rel={newTab ? 'noopener noreferrer' : undefined}
|
||||
className="underline font-joey-bold hover:opacity-70 transition-opacity"
|
||||
>
|
||||
{buttonLabel}
|
||||
</a>
|
||||
)}
|
||||
{/* Centered content — px-10 leaves room for the dismiss button on both sides */}
|
||||
<div className="flex items-center justify-center gap-3 text-sm font-joey text-center flex-wrap">
|
||||
<span>{text}</span>
|
||||
{buttonLabel && href && (
|
||||
<a
|
||||
href={href}
|
||||
target={newTab ? '_blank' : undefined}
|
||||
rel={newTab ? 'noopener noreferrer' : undefined}
|
||||
className="underline font-joey-bold hover:opacity-70 transition-opacity whitespace-nowrap"
|
||||
>
|
||||
{buttonLabel}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Dismiss — truly absolute so it doesn't affect centering */}
|
||||
{dismissible && (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
✕
|
||||
|
||||
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;
|
||||
};
|
||||
/**
|
||||
* 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)
|
||||
*/
|
||||
@ -2813,7 +2851,7 @@ export interface Footer {
|
||||
}[]
|
||||
| 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?:
|
||||
| {
|
||||
@ -2839,7 +2877,6 @@ export interface Footer {
|
||||
* Använd {year} för aktuellt årtal
|
||||
*/
|
||||
bottomLeftText?: string | null;
|
||||
bottomRightText?: string | null;
|
||||
updatedAt?: string | null;
|
||||
createdAt?: string | null;
|
||||
}
|
||||
@ -3014,6 +3051,28 @@ export interface FooterSelect<T extends boolean = true> {
|
||||
reference?: 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?:
|
||||
| T
|
||||
| {
|
||||
@ -3049,7 +3108,6 @@ export interface FooterSelect<T extends boolean = true> {
|
||||
id?: T;
|
||||
};
|
||||
bottomLeftText?: T;
|
||||
bottomRightText?: T;
|
||||
updatedAt?: T;
|
||||
createdAt?: T;
|
||||
globalType?: T;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user