wwwfiberdirekt/src/globals/PopupAnnouncement/Component.tsx

207 lines
7.0 KiB
TypeScript

'use client'
import React, { useState, useEffect } from 'react'
import Image from 'next/image'
import type { Media, Page } from '@/payload-types'
type LinkGroup = {
type?: 'reference' | 'custom' | null
reference?: Page | string | number | null
url?: string | null
newTab?: boolean | null
}
// specificPages is now a relationship array — each item is a populated Page or just an id
type SpecificPage = Page | string | number
interface PopupAnnouncementProps {
enabled?: boolean
heading?: string
subheading?: string
body?: string
ctaText?: string
ctaLink?: LinkGroup | null
image?: Media | string | null
badgeText?: string
theme?: 'light' | 'dark'
showOnPages?: 'all' | 'home' | 'specific'
specificPages?: SpecificPage[] | null
dismissDays?: number
}
function resolveUrl(link?: LinkGroup | null): string | null {
if (!link) return null
if (link.type === 'reference' && link.reference) {
if (typeof link.reference === 'object' && 'slug' in link.reference) {
return link.reference.slug ? `/${link.reference.slug}` : null
}
return null
}
return link.url ?? null
}
function resolveSpecificPageSlugs(pages?: SpecificPage[] | null): string[] {
if (!pages) return []
return pages.flatMap((p) => {
if (typeof p === 'object' && p !== null && 'slug' in p && typeof p.slug === 'string') {
return [`/${p.slug}`]
}
return []
})
}
export const PopupAnnouncementComponent: React.FC<PopupAnnouncementProps> = ({
enabled,
heading,
subheading,
body,
ctaText = 'Läs mer',
ctaLink,
image,
badgeText,
theme = 'light',
showOnPages = 'all',
specificPages,
dismissDays = 7,
}) => {
const [isVisible, setIsVisible] = useState(false)
const [isClosing, setIsClosing] = useState(false)
useEffect(() => {
if (!enabled) return
// Check if dismissed
const dismissedAt = localStorage.getItem('fd-popup-dismissed')
if (dismissedAt) {
const dismissDate = new Date(dismissedAt)
const now = new Date()
const daysDiff = (now.getTime() - dismissDate.getTime()) / (1000 * 60 * 60 * 24)
if (daysDiff < dismissDays) return
}
// Check page targeting
const path = window.location.pathname
if (showOnPages === 'home' && path !== '/') return
if (showOnPages === 'specific') {
const slugs = resolveSpecificPageSlugs(specificPages)
if (!slugs.includes(path)) return
}
// Show after short delay
const timer = setTimeout(() => setIsVisible(true), 1500)
return () => clearTimeout(timer)
}, [enabled, showOnPages, specificPages, dismissDays])
const handleClose = () => {
setIsClosing(true)
localStorage.setItem('fd-popup-dismissed', new Date().toISOString())
setTimeout(() => setIsVisible(false), 300)
}
if (!isVisible || !enabled) return null
const media = typeof image === 'object' && image !== null ? (image as Media) : null
const isDark = theme === 'dark'
const href = resolveUrl(ctaLink)
const newTab = ctaLink?.newTab ?? false
// ── FIX: Resolve optimized image URL via Payload size or fall back to original ──
const imageSrc = media?.sizes?.medium?.url || media?.sizes?.large?.url || media?.url || ''
const imageWidth = media?.sizes?.medium?.width || media?.sizes?.large?.width || media?.width || 560
const imageHeight = media?.sizes?.medium?.height || media?.sizes?.large?.height || media?.height || 720
const isSvg = media?.mimeType === 'image/svg+xml'
return (
<>
{/* Backdrop */}
<div
className={`fixed inset-0 bg-black/50 z-[9998] transition-opacity duration-300 ${isClosing ? 'opacity-0' : 'opacity-100'}`}
onClick={handleClose}
/>
{/* Popup */}
<div
className={`fixed inset-0 z-[9999] flex items-center justify-center p-4 md:p-6 pointer-events-none transition-all duration-300 ${isClosing ? 'opacity-0 scale-95' : 'opacity-100 scale-100'}`}
>
<div
className={`pointer-events-auto relative w-full max-w-[720px] rounded-2xl overflow-hidden shadow-2xl flex flex-col md:flex-row ${isDark ? 'bg-fd-navy' : 'bg-white'}`}
onClick={(e) => e.stopPropagation()}
>
{/* Close button */}
<button
onClick={handleClose}
className={`absolute top-4 right-4 w-10 h-10 flex items-center justify-center rounded-full transition-all duration-200 z-10 ${isDark ? 'text-white/60 hover:text-white hover:bg-white/10' : 'text-gray-400 hover:text-gray-800 hover:bg-gray-100'}`}
aria-label="Stäng"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
{/* Content */}
<div className="flex-1 p-6 md:p-8 flex flex-col justify-center">
{badgeText && (
<span
className={`inline-block self-start px-3 py-1 rounded-full text-fd-xs font-joey-bold uppercase tracking-wider mb-4 ${isDark ? 'bg-fd-yellow/20 text-fd-yellow' : 'bg-fd-navy/10 text-fd-navy'}`}
>
{badgeText}
</span>
)}
{heading && (
<h3 className={`font-joey-heavy text-fd-h2 mb-2 ${isDark ? 'text-white' : 'text-fd-navy'}`}>
{heading}
</h3>
)}
{subheading && (
<p className={`font-joey-medium text-fd-h4 mb-3 ${isDark ? 'text-fd-yellow' : 'text-fd-navy/80'}`}>
{subheading}
</p>
)}
{body && (
<p className={`font-joey text-fd-body mb-6 ${isDark ? 'text-white/80' : 'text-fd-text-secondary'}`}>
{body}
</p>
)}
{ctaText && href && (
<a
href={href}
target={newTab ? '_blank' : undefined}
rel={newTab ? 'noopener noreferrer' : undefined}
className="fd-btn-primary self-start"
>
{ctaText}
</a>
)}
</div>
{/* Optional image — FIX: use next/image instead of raw <img> */}
{media?.url && imageSrc && (
<div className="hidden md:block md:w-[280px] relative">
{isSvg ? (
// SVGs skip optimization (same as FDImage convention)
// eslint-disable-next-line @next/next/no-img-element
<img
src={media.url}
alt={media.alt || ''}
className="w-full h-full object-cover"
loading="lazy"
/>
) : (
<Image
src={imageSrc}
alt={media.alt || ''}
width={imageWidth}
height={imageHeight}
className="w-full h-full object-cover"
sizes="280px"
quality={80}
/>
)}
</div>
)}
</div>
</div>
</>
)
}