| 1 |
import { safeParseJson } from '@shared/lib/parsing'; |
| 2 |
import { track } from '@shared/lib/track'; |
| 3 |
import { useEffect, useState } from '@wordpress/element'; |
| 4 |
import { __ } from '@wordpress/i18n'; |
| 5 |
import { close, Icon } from '@wordpress/icons'; |
| 6 |
import { useNotificationsStore } from './notifications'; |
| 7 |
import { resolveNotificationLink } from './partner-notification-link'; |
| 8 |
|
| 9 |
export const COLOR_DEFAULTS = { |
| 10 |
'color-background': '#ffffff', |
| 11 |
'color-border': '#d1d5db', |
| 12 |
'color-text': '#1e1e1e', |
| 13 |
'color-button-background': '#1e1e1e', |
| 14 |
'color-button-text': '#ffffff', |
| 15 |
'color-button-hover-background': '#000000', |
| 16 |
'color-button-hover-text': '#ffffff', |
| 17 |
'color-button-close-background': '#f3f4f6', |
| 18 |
'color-button-close-text': '#1e1e1e', |
| 19 |
'color-button-close-hover-background': '#d1d5db', |
| 20 |
'color-button-close-hover-text': '#1e1e1e', |
| 21 |
}; |
| 22 |
|
| 23 |
const CSS_VARS = { |
| 24 |
'color-background': '--ext-pn-bg', |
| 25 |
'color-border': '--ext-pn-border', |
| 26 |
'color-text': '--ext-pn-text', |
| 27 |
'color-button-background': '--ext-pn-btn-bg', |
| 28 |
'color-button-text': '--ext-pn-btn-text', |
| 29 |
'color-button-hover-background': '--ext-pn-btn-hover-bg', |
| 30 |
'color-button-hover-text': '--ext-pn-btn-hover-text', |
| 31 |
'color-button-close-background': '--ext-pn-close-bg', |
| 32 |
'color-button-close-text': '--ext-pn-close-icon', |
| 33 |
'color-button-close-hover-background': '--ext-pn-close-hover-bg', |
| 34 |
'color-button-close-hover-text': '--ext-pn-close-hover-icon', |
| 35 |
}; |
| 36 |
|
| 37 |
const colorStyle = (notification) => |
| 38 |
Object.fromEntries( |
| 39 |
Object.entries(CSS_VARS).map(([field, cssVar]) => [ |
| 40 |
cssVar, |
| 41 |
notification[field] || COLOR_DEFAULTS[field], |
| 42 |
]), |
| 43 |
); |
| 44 |
|
| 45 |
export const PartnerNotification = ({ slot }) => { |
| 46 |
const { |
| 47 |
dismissBanner, |
| 48 |
isDismissedBanner, |
| 49 |
hasViewedNotification, |
| 50 |
recordNotificationView, |
| 51 |
} = useNotificationsStore(); |
| 52 |
const adminUrl = window.extSharedData?.adminUrl ?? ''; |
| 53 |
|
| 54 |
const notifications = safeParseJson( |
| 55 |
window.extSharedData?.partnerNotifications, |
| 56 |
[], |
| 57 |
); |
| 58 |
// Frozen at mount so dismissing doesn't reveal the next notification until reload. |
| 59 |
const [notification] = useState(() => |
| 60 |
notifications.find( |
| 61 |
(item) => item?.slots?.includes(slot) && !isDismissedBanner(item.slug), |
| 62 |
), |
| 63 |
); |
| 64 |
|
| 65 |
const slug = notification?.slug; |
| 66 |
const page = window.pagenow ?? ''; |
| 67 |
useEffect(() => { |
| 68 |
if (!slug) return; |
| 69 |
if (hasViewedNotification(slug, slot, page)) return; |
| 70 |
track('partner_notification_view', { slug, slot, page }); |
| 71 |
recordNotificationView(slug, slot, page); |
| 72 |
}, [slug, slot, page, hasViewedNotification, recordNotificationView]); |
| 73 |
|
| 74 |
if (!notification || isDismissedBanner(slug)) return null; |
| 75 |
|
| 76 |
const { title, content, image } = notification; |
| 77 |
const buttonLabel = notification['button-label']; |
| 78 |
const linkType = notification['button-link-type']; |
| 79 |
const { href, external } = resolveNotificationLink(notification, adminUrl); |
| 80 |
|
| 81 |
const dismiss = () => { |
| 82 |
track('partner_notification_dismiss', { slug, slot, page }); |
| 83 |
dismissBanner(slug); |
| 84 |
}; |
| 85 |
|
| 86 |
return ( |
| 87 |
<div className="extendify-shared"> |
| 88 |
<div |
| 89 |
style={colorStyle(notification)} |
| 90 |
className="relative h-full min-h-32 w-full rounded-sm border bg-(--ext-pn-bg) px-5 py-5 text-base text-(--ext-pn-text) lg:px-8 lg:py-6 border-(--ext-pn-border)" |
| 91 |
data-test="assist-partner-notification" |
| 92 |
> |
| 93 |
<button |
| 94 |
type="button" |
| 95 |
aria-label={__('Dismiss notification', 'extendify-local')} |
| 96 |
onClick={dismiss} |
| 97 |
className="absolute right-0 top-0 flex h-8 w-8 items-center justify-center rounded-bl rounded-se text-center p-1.5 rtl:left-0 rtl:right-auto rtl:rounded-bl-none rtl:rounded-br bg-(--ext-pn-close-bg) text-(--ext-pn-close-icon) hover:bg-(--ext-pn-close-hover-bg) hover:text-(--ext-pn-close-hover-icon)" |
| 98 |
> |
| 99 |
<Icon icon={close} size={32} className="fill-current" /> |
| 100 |
</button> |
| 101 |
<div className="flex flex-col items-start gap-4"> |
| 102 |
{image && ( |
| 103 |
<img src={image} alt="" className="max-h-8 w-auto rounded-xs" /> |
| 104 |
)} |
| 105 |
<div> |
| 106 |
<div className="text-lg font-semibold">{title}</div> |
| 107 |
<div className="mt-1 text-sm">{content}</div> |
| 108 |
</div> |
| 109 |
{buttonLabel && href && ( |
| 110 |
<a |
| 111 |
href={href} |
| 112 |
target={external ? '_blank' : undefined} |
| 113 |
rel={external ? 'noreferrer' : undefined} |
| 114 |
onClick={() => |
| 115 |
track('partner_notification_click', { |
| 116 |
slug, |
| 117 |
slot, |
| 118 |
linkType, |
| 119 |
page, |
| 120 |
}) |
| 121 |
} |
| 122 |
className="inline-flex h-10 cursor-pointer items-center rounded-xs px-4 text-sm no-underline bg-(--ext-pn-btn-bg) text-(--ext-pn-btn-text) hover:bg-(--ext-pn-btn-hover-bg) hover:text-(--ext-pn-btn-hover-text)" |
| 123 |
> |
| 124 |
{buttonLabel} |
| 125 |
</a> |
| 126 |
)} |
| 127 |
</div> |
| 128 |
</div> |
| 129 |
</div> |
| 130 |
); |
| 131 |
}; |
| 132 |
|