Skip to content

Commit 167606b

Browse files
authored
feat: reintroduce promotional CTA banner + rebrand cleanup (#609)
Adds PromoBanner React component that fetches promotional offers from weDevsOfficial/wppm-util/promotions.json and renders an editorial-style banner on /projects and /premium pages. Respects start/end date window (with timezone-aware parser supporting EST/EDT/PST/PDT/UTC/GMT suffixes) and offers per-promo dismiss via localStorage. Hidden expired-promo testing via ?pm_promo_preview=1 query flag. Other changes: - TopBar "Share your idea" link now points to feedback.wedevs.com/b/project-manager (replaces pm.canny.io/ideas). - ProUpgradeModal title now reads "Project Manager Pro" (drops "WP" prefix). - PremiumPage hero gradient switched to inline linear-gradient to bypass Tailwind --tw-gradient-stops chain fragility under important:true config (was rendering invisible in light mode).
1 parent 57b637f commit 167606b

5 files changed

Lines changed: 213 additions & 4 deletions

File tree

views/assets/src/components/common/ProUpgradeModal.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ function ProUpgradeModal() {
145145
<span style={{ color: '#ff9000', fontSize: '30px', fontWeight: 500, lineHeight: '1.6' }}>{__('Upgrade to', 'wedevs-project-manager')}</span>
146146
</div>
147147
<h2 style={{ fontSize: '30px', fontWeight: 400, lineHeight: '1.6', margin: 0, color: 'var(--pm-text-primary)' }}>
148-
{__('WP Project Manager', 'wedevs-project-manager')} <span style={{ fontWeight: 700 }}>{__('Pro', 'wedevs-project-manager')}</span>
148+
{__('Project Manager', 'wedevs-project-manager')} <span style={{ fontWeight: 700 }}>{__('Pro', 'wedevs-project-manager')}</span>
149149
</h2>
150150
<p style={{ fontSize: '20px', fontWeight: 400, color: 'var(--pm-text-muted)', lineHeight: '1.6', margin: 0 }}>
151151
{__('unlock and take advantage of our premium features', 'wedevs-project-manager')} 🎉
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { __ } from '@wordpress/i18n'
2+
import React, { useEffect, useState, useMemo, useCallback } from 'react'
3+
import { X, ArrowRight } from 'lucide-react'
4+
5+
const PROMO_URL = 'https://raw.githubusercontent.com/weDevsOfficial/wppm-util/master/promotions.json'
6+
const STORAGE_PREFIX = 'pm-promo-dismissed:'
7+
const FETCH_TIMEOUT_MS = 6000
8+
9+
const TZ_OFFSETS = { EST: '-05:00', EDT: '-04:00', PST: '-08:00', PDT: '-07:00', UTC: '+00:00', GMT: '+00:00' }
10+
11+
function parsePromoDate(str) {
12+
if (!str) return NaN
13+
const m = String(str).trim().match(/^(\d{4}-\d{2}-\d{2})[ T](\d{1,2}):(\d{2}):(\d{2})(?:\s+([A-Z]{3,4}))?$/)
14+
if (!m) return Date.parse(str)
15+
const [, date, h, mm, ss, tz] = m
16+
const hour = h.padStart(2, '0')
17+
const offset = tz && TZ_OFFSETS[tz] ? TZ_OFFSETS[tz] : '+00:00'
18+
return Date.parse(`${date}T${hour}:${mm}:${ss}${offset}`)
19+
}
20+
21+
function isWithinWindow(promo) {
22+
if (typeof window !== 'undefined' && /[?&]pm_promo_preview=1\b/.test(window.location.search)) {
23+
return true
24+
}
25+
const now = Date.now()
26+
const start = parsePromoDate(promo.start_date)
27+
const end = parsePromoDate(promo.end_date)
28+
if (!Number.isNaN(start) && now < start) return false
29+
if (!Number.isNaN(end) && now > end) return false
30+
return true
31+
}
32+
33+
function extractDiscount(text) {
34+
if (!text) return null
35+
const m = String(text).match(/(\d{2,3})\s*%/)
36+
return m ? `${m[1]}% OFF` : null
37+
}
38+
39+
function daysRemaining(endStr) {
40+
const end = parsePromoDate(endStr)
41+
if (Number.isNaN(end)) return null
42+
const diff = Math.ceil((end - Date.now()) / 86400000)
43+
return diff > 0 && diff <= 30 ? diff : null
44+
}
45+
46+
export function PromoBanner({ placement = 'projects' }) {
47+
const [promo, setPromo] = useState(null)
48+
const [dismissed, setDismissed] = useState(false)
49+
50+
const dismissKey = useMemo(
51+
() => (promo?.key ? `${STORAGE_PREFIX}${promo.key}` : null),
52+
[promo]
53+
)
54+
55+
useEffect(() => {
56+
let cancelled = false
57+
const controller = new AbortController()
58+
const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS)
59+
60+
fetch(PROMO_URL, { signal: controller.signal, cache: 'no-store' })
61+
.then(r => {
62+
if (!r.ok) throw new Error(`HTTP ${r.status}`)
63+
return r.json()
64+
})
65+
.then(data => {
66+
if (cancelled) return
67+
if (!data || !data.key) {
68+
console.warn('[PM PromoBanner] payload missing key', data)
69+
return
70+
}
71+
if (!isWithinWindow(data)) {
72+
console.info('[PM PromoBanner] promo outside window', data.key, data.start_date, data.end_date)
73+
return
74+
}
75+
const key = `${STORAGE_PREFIX}${data.key}`
76+
if (localStorage.getItem(key) === '1') {
77+
setDismissed(true)
78+
return
79+
}
80+
setPromo(data)
81+
})
82+
.catch(err => {
83+
if (err?.name !== 'AbortError') {
84+
console.warn('[PM PromoBanner] fetch failed', err)
85+
}
86+
})
87+
.finally(() => clearTimeout(timer))
88+
89+
return () => {
90+
cancelled = true
91+
controller.abort()
92+
clearTimeout(timer)
93+
}
94+
}, [])
95+
96+
const handleDismiss = useCallback(() => {
97+
if (dismissKey) localStorage.setItem(dismissKey, '1')
98+
setDismissed(true)
99+
}, [dismissKey])
100+
101+
if (dismissed || !promo) return null
102+
103+
const action = promo.action_url
104+
? `${promo.action_url}${promo.action_url.includes('?') ? '&' : '?'}utm_content=pm-${placement}-banner`
105+
: null
106+
107+
const discount = extractDiscount(promo.title) || extractDiscount(promo.content)
108+
const daysLeft = daysRemaining(promo.end_date)
109+
110+
return (
111+
<div
112+
className="pm-promo-banner relative rounded-xl border overflow-hidden"
113+
style={{
114+
background: '#FAFAFB',
115+
borderColor: '#E5E7EB',
116+
}}
117+
>
118+
<span
119+
aria-hidden
120+
className="absolute left-0 top-0 bottom-0"
121+
style={{ width: '3px', background: '#7C3AED' }}
122+
/>
123+
124+
<button
125+
type="button"
126+
onClick={handleDismiss}
127+
aria-label={__('Dismiss', 'wedevs-project-manager')}
128+
className="absolute top-2 right-2 z-20 p-1.5 rounded text-pm-text-muted hover:text-pm-text-primary bg-white/80 hover:bg-pm-hover backdrop-blur-sm transition-colors"
129+
>
130+
<X className="h-3.5 w-3.5" />
131+
</button>
132+
133+
<div className="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-5 px-5 py-4 pl-6 pr-10 sm:pr-12">
134+
<div className="flex items-center gap-3 flex-1 min-w-0">
135+
<img
136+
src={`${typeof PM_Vars !== 'undefined' ? PM_Vars.dir_url : '/wp-content/plugins/wedevs-project-manager/'}.wordpress-org/icon-128x128.gif`}
137+
alt=""
138+
className="shrink-0 rounded-lg"
139+
style={{ height: '56px', width: '56px' }}
140+
/>
141+
{discount && (
142+
<span
143+
className="shrink-0 inline-flex items-baseline gap-1 px-3 py-1.5 rounded-md"
144+
style={{
145+
background: 'linear-gradient(135deg, #FF8A00 0%, #FF5C00 100%)',
146+
color: '#fff',
147+
boxShadow: '0 4px 12px -2px rgba(255,92,0,0.45)',
148+
}}
149+
>
150+
<span style={{ fontSize: '20px', fontWeight: 800, lineHeight: 1, letterSpacing: '-0.02em' }}>
151+
{discount.replace(' OFF', '')}
152+
</span>
153+
<span style={{ fontSize: '11px', fontWeight: 700, letterSpacing: '0.08em' }}>
154+
OFF
155+
</span>
156+
</span>
157+
)}
158+
<span className="text-sm font-semibold text-pm-text-primary truncate">
159+
{promo.title}
160+
</span>
161+
{daysLeft != null && (
162+
<span className="hidden md:inline text-[12px] text-pm-text-muted shrink-0">
163+
· {daysLeft === 1
164+
? __('ends tomorrow', 'wedevs-project-manager')
165+
: `${daysLeft} ${__('days left', 'wedevs-project-manager')}`}
166+
</span>
167+
)}
168+
</div>
169+
170+
{action && (
171+
<a
172+
href={action}
173+
target="_blank"
174+
rel="noopener noreferrer"
175+
className="pm-promo-cta inline-flex items-center justify-center gap-2 self-start sm:self-auto shrink-0 px-5 py-2.5 rounded-md font-semibold text-[13px] tracking-[0.01em] whitespace-nowrap no-underline transition-all"
176+
style={{
177+
background: '#7C3AED',
178+
color: '#fff',
179+
boxShadow: '0 1px 2px rgba(124,58,237,0.25), 0 4px 12px -2px rgba(124,58,237,0.35)',
180+
border: '1px solid #6D28D9',
181+
}}
182+
onMouseEnter={e => {
183+
e.currentTarget.style.background = '#6D28D9'
184+
e.currentTarget.style.boxShadow = '0 1px 2px rgba(124,58,237,0.3), 0 8px 18px -2px rgba(124,58,237,0.5)'
185+
}}
186+
onMouseLeave={e => {
187+
e.currentTarget.style.background = '#7C3AED'
188+
e.currentTarget.style.boxShadow = '0 1px 2px rgba(124,58,237,0.25), 0 4px 12px -2px rgba(124,58,237,0.35)'
189+
}}
190+
>
191+
<span>{promo.action_title || __('Upgrade to Pro', 'wedevs-project-manager')}</span>
192+
<ArrowRight className="h-3.5 w-3.5" />
193+
</a>
194+
)}
195+
</div>
196+
</div>
197+
)
198+
}
199+
200+
export default PromoBanner

views/assets/src/components/layout/TopBar.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ export function TopBar() {
278278

279279
{/* Share Your Idea */}
280280
<a
281-
href="https://pm.canny.io/ideas"
281+
href="https://feedback.wedevs.com/b/project-manager"
282282
target="_blank"
283283
rel="noopener noreferrer"
284284
className="hidden md:inline-flex items-center gap-1 text-sm text-pm-text-muted hover:text-pm-accent transition-colors shrink-0"

views/assets/src/components/projects/PremiumPage.jsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
ArrowRight,
3030
Globe,
3131
} from "lucide-react";
32+
import { PromoBanner } from "@components/common/PromoBanner";
3233

3334
const UPGRADE_URL =
3435
"https://wedevs.com/wp-project-manager-pro/pricing/?utm_source=wpdashboard&utm_medium=premium-page";
@@ -138,9 +139,14 @@ export default function PremiumPage() {
138139

139140
return (
140141
<div className="max-w-[1400px] mx-auto p-4 sm:p-6 space-y-8">
142+
<PromoBanner placement="premium" />
143+
141144
{/* ── Hero Banner ── */}
142-
<div className="relative rounded-2xl overflow-hidden bg-gradient-to-br from-purple-600 via-indigo-600 to-blue-600 px-8 py-10 md:px-12 md:py-14 text-center text-white">
143-
<div className="absolute inset-0 bg-[radial-gradient(circle_at_30%_50%,rgba(255,255,255,0.1),transparent_70%)]" />
145+
<div
146+
className="relative rounded-2xl overflow-hidden px-8 py-10 md:px-12 md:py-14 text-center text-white"
147+
style={{ background: 'linear-gradient(135deg, #7C3AED 0%, #4F46E5 50%, #2563EB 100%)' }}
148+
>
149+
<div className="absolute inset-0" style={{ background: 'radial-gradient(circle at 30% 50%, rgba(255,255,255,0.12), transparent 70%)' }} />
144150
<div className="relative z-10">
145151
<span className="inline-flex items-center gap-1.5 bg-white/20 backdrop-blur-sm text-white text-sm font-medium px-3 py-1 rounded-full mb-3">
146152
<Sparkles className="h-4 w-4" />

views/assets/src/components/projects/ProjectsPage/index.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ import {
9191

9292
import AiCreateDialog from "../AiCreateDialog";
9393
import { ProjectCreateSheet } from "../ProjectCreateSheet";
94+
import { PromoBanner } from "@components/common/PromoBanner";
9495

9596
import { formatPmDate } from "@lib/pm-utils";
9697
import {
@@ -693,6 +694,8 @@ export default function ProjectsPage() {
693694

694695
return (
695696
<div className="max-w-[1400px] mx-auto p-4 sm:p-6 space-y-6">
697+
<PromoBanner placement="projects" />
698+
696699
<div className="flex items-center justify-between flex-wrap gap-2">
697700
<h1 className="text-2xl font-bold text-pm-text-primary">
698701
{__("Projects", 'wedevs-project-manager')}

0 commit comments

Comments
 (0)