207 lines
7.0 KiB
TypeScript
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>
|
|
</>
|
|
)
|
|
}
|