Skip to content

Commit 221270e

Browse files
Add Framna announcement popup (#5428)
- Two-panel modal: green (#1BC866) left panel with Framna logo, white right panel - Shows 500ms after page load event (minimises page load impact) - Persists to localStorage: shows once per 30 days - Date gate: production shows from 18.03.2026; staging shows immediately - Visit Framna button links to https://framna.com/ - All browser APIs guarded inside useEffect (SSR-safe) Closes #5423 Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Piotr Mionskowski <miensol@users.noreply.github.com>
1 parent a740a66 commit 221270e

File tree

2 files changed

+185
-0
lines changed

2 files changed

+185
-0
lines changed
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import React, { useEffect, useState } from 'react'
2+
3+
const STORAGE_KEY = 'framna_announcement_shown_at'
4+
const SHOW_DURATION_MS = 30 * 24 * 60 * 60 * 1000 // 30 days
5+
const PRODUCTION_SHOW_DATE = new Date('2026-03-18T00:00:00')
6+
7+
function shouldShowPopup(): boolean {
8+
const isStaging = process.env.GATSBY_ACTIVE_ENV === 'staging'
9+
const now = new Date()
10+
11+
if (!isStaging && now < PRODUCTION_SHOW_DATE) {
12+
return false
13+
}
14+
15+
try {
16+
const shownAt = localStorage.getItem(STORAGE_KEY)
17+
if (!shownAt) return true
18+
const shownAtMs = parseInt(shownAt, 10)
19+
return now.getTime() - shownAtMs > SHOW_DURATION_MS
20+
} catch {
21+
return true
22+
}
23+
}
24+
25+
function markShown(): void {
26+
try {
27+
localStorage.setItem(STORAGE_KEY, Date.now().toString())
28+
} catch {
29+
// ignore storage errors
30+
}
31+
}
32+
33+
export const FramnaAnnouncementPopup: React.FC = () => {
34+
const [visible, setVisible] = useState(false)
35+
36+
useEffect(() => {
37+
let timer: ReturnType<typeof setTimeout> | undefined
38+
39+
const show = () => {
40+
timer = setTimeout(() => {
41+
if (shouldShowPopup()) {
42+
setVisible(true)
43+
markShown()
44+
}
45+
}, 500)
46+
}
47+
48+
if (document.readyState === 'complete') {
49+
show()
50+
} else {
51+
window.addEventListener('load', show, { once: true })
52+
}
53+
54+
return () => {
55+
if (timer !== undefined) {
56+
clearTimeout(timer)
57+
}
58+
window.removeEventListener('load', show)
59+
}
60+
}, [])
61+
62+
if (!visible) return null
63+
64+
return (
65+
<div
66+
style={{
67+
position: 'fixed',
68+
inset: 0,
69+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
70+
display: 'flex',
71+
alignItems: 'center',
72+
justifyContent: 'center',
73+
zIndex: 9999,
74+
}}
75+
onClick={() => setVisible(false)}
76+
>
77+
<div
78+
style={{
79+
display: 'flex',
80+
borderRadius: '16px',
81+
overflow: 'hidden',
82+
maxWidth: '700px',
83+
width: '90%',
84+
backgroundColor: '#fff',
85+
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
86+
}}
87+
onClick={e => e.stopPropagation()}
88+
>
89+
{/* Left green panel */}
90+
<div
91+
style={{
92+
backgroundColor: '#1BC866',
93+
flex: '0 0 45%',
94+
display: 'flex',
95+
alignItems: 'center',
96+
justifyContent: 'center',
97+
padding: '48px 32px',
98+
minHeight: '320px',
99+
}}
100+
>
101+
<img
102+
src='/images/why-us/timeline/framna.svg'
103+
alt='Framna'
104+
style={{ maxWidth: '160px', width: '100%' }}
105+
/>
106+
</div>
107+
108+
{/* Right white panel */}
109+
<div
110+
style={{
111+
flex: '1',
112+
padding: '40px 36px 40px 40px',
113+
display: 'flex',
114+
flexDirection: 'column',
115+
justifyContent: 'center',
116+
position: 'relative',
117+
}}
118+
>
119+
<button
120+
onClick={() => setVisible(false)}
121+
aria-label='Close'
122+
style={{
123+
position: 'absolute',
124+
top: '16px',
125+
right: '16px',
126+
background: 'none',
127+
border: 'none',
128+
cursor: 'pointer',
129+
fontSize: '20px',
130+
lineHeight: '1',
131+
padding: '4px',
132+
color: '#000',
133+
}}
134+
>
135+
136+
</button>
137+
138+
<h2
139+
style={{
140+
margin: '0 0 20px',
141+
fontSize: '28px',
142+
fontWeight: 700,
143+
lineHeight: '1.2',
144+
}}
145+
>
146+
Bright Inventions is now Framna
147+
</h2>
148+
149+
<p
150+
style={{
151+
margin: '0 0 32px',
152+
fontSize: '16px',
153+
lineHeight: '1.6',
154+
color: '#333',
155+
}}
156+
>
157+
We partner with industry leaders (and those about to be) to create digital products that
158+
define markets, reshape industries, and drive meaningful growth.
159+
</p>
160+
161+
<a
162+
href='https://framna.com/'
163+
target='_blank'
164+
rel='noopener noreferrer'
165+
style={{
166+
display: 'inline-block',
167+
backgroundColor: '#0a0a0a',
168+
color: '#fff',
169+
padding: '14px 28px',
170+
borderRadius: '32px',
171+
textDecoration: 'none',
172+
fontWeight: 600,
173+
fontSize: '15px',
174+
alignSelf: 'flex-start',
175+
}}
176+
>
177+
Visit Framna
178+
</a>
179+
</div>
180+
</div>
181+
</div>
182+
)
183+
}

src/layout/Page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { useLocation } from '@reach/router'
1111
import { MDXComponentsWrapper } from '../mdx'
1212
import CookiesNotice from '../analytics/cookies-notice'
1313
import { useTranslation } from 'react-i18next'
14+
import { FramnaAnnouncementPopup } from '../components/shared/FramnaAnnouncementPopup'
1415

1516
export const Page: React.FC<PropsWithChildren<{ className?: string }>> = ({ children, className }) => {
1617
const [mobileMenuOpened, setMobileMenuOpened] = useState(false)
@@ -34,6 +35,7 @@ export const Page: React.FC<PropsWithChildren<{ className?: string }>> = ({ chil
3435
<TopNavigation path={pathname} toggled={setMobileMenuOpened} />
3536
<MDXComponentsWrapper>{children}</MDXComponentsWrapper>
3637
<CookiesNotice />
38+
<FramnaAnnouncementPopup />
3739
<Footer />
3840
</div>
3941
)

0 commit comments

Comments
 (0)