PluginProbe
Extendify / 3.2.1
Extendify v3.2.1
3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 0.6.0 All 127 releases
extendify / src / Recommendations / components / RecommendationCard.jsx

RecommendationCard.jsx in Extendify 3.2.1, at src/Recommendations/components/RecommendationCard.jsx

267 lines 7.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import { recordActivity } from '@recommendations/utils/record-activity';
2 import { recordPluginActivity } from '@shared/api/DataApi';
3 import { activatePlugin, installPlugin } from '@shared/api/wp';
4 import { retryOperation, sleep } from '@shared/lib/utils';
5 import { Button } from '@wordpress/components';
6 import { useCallback, useEffect, useState } from '@wordpress/element';
7 import { decodeEntities } from '@wordpress/html-entities';
8 import { __, _x, sprintf } from '@wordpress/i18n';
9 import { caution, check, external, Icon } from '@wordpress/icons';
10
11 export const RecommendationCard = ({
12 slug: product,
13 title,
14 description,
15 ctaContent,
16 provider,
17 image,
18 ctaType,
19 ctaPluginSlug,
20 ctaExternalLink,
21 ctaInternalLink,
22 priceTag,
23 }) => {
24 useEffect(() => {
25 recordActivity({ slot: 'plugin-search', event: 'view', product });
26 }, [product]);
27
28 return (
29 <div
30 className="flex flex-col rounded-sm border border-gray-300"
31 data-test="extendify-recommendations-card"
32 >
33 <div className="row-auto grid grow grid-cols-[2fr_3fr] grid-rows-[min-content_1fr] gap-x-5 gap-y-3 p-5 xs:grid-cols-[8rem_1fr]">
34 <div className="row-span-1 xs:row-span-2">
35 {image &&
36 (ctaType === 'plugin' ? (
37 <a
38 onClick={() =>
39 recordActivity({
40 slot: 'plugin-search',
41 event: 'click-logo',
42 product,
43 })
44 }
45 // These WP classes (thickbox open-plugin-details-modal) are needed for the link to open in an iframe like WP does.
46 className="thickbox open-plugin-details-modal block no-underline"
47 // The hardcoded iframe dimensions are the default ones, but some WP magic makes them responsive.
48 href={`${window.extSharedData?.adminUrl}/plugin-install.php?tab=plugin-information&plugin=${ctaPluginSlug}&TB_iframe=true&width=600&height=550`}
49 >
50 <img
51 className="w-full xs:min-h-[8rem]"
52 src={image}
53 alt={title}
54 />
55 </a>
56 ) : (
57 <img className="w-full xs:min-h-[8rem]" src={image} alt={title} />
58 ))}
59 </div>
60 <div className="flex flex-col justify-center xs:justify-start">
61 <h3 className="m-0 text-lg leading-tight text-wp-theme-main">
62 {ctaType === 'plugin' ? (
63 <a
64 onClick={() =>
65 recordActivity({
66 slot: 'plugin-search',
67 event: 'click-title',
68 product,
69 })
70 }
71 // These WP classes (thickbox open-plugin-details-modal) are needed for the link to open in an iframe like WP does.
72 className="thickbox open-plugin-details-modal no-underline focus:shadow-none"
73 // The hardcoded iframe dimensions are the default ones, but some WP magic makes them responsive.
74 href={`${window.extSharedData?.adminUrl}/plugin-install.php?tab=plugin-information&plugin=${ctaPluginSlug}&TB_iframe=true&width=600&height=550`}
75 >
76 {decodeEntities(title)}
77 </a>
78 ) : (
79 decodeEntities(title)
80 )}
81 </h3>
82 <p className="m-0 mt-1 text-xs italic">
83 {sprintf(
84 // translators: %s is a name
85 _x(
86 'By %s',
87 'Preposition for "By Author Name"',
88 'extendify-local',
89 ),
90 provider,
91 )}
92 </p>
93 </div>
94 <p className="col-span-2 m-0 text-sm xs:col-span-1">
95 {decodeEntities(description)}
96 </p>
97 </div>
98 <div className="flex min-h-14 shrink-0 flex-col items-center justify-center p-3 px-5 xxs:flex-row xxs:justify-end">
99 {priceTag && (
100 <p
101 className="m-0 mb-3 xxs:mb-0 xxs:mr-4"
102 dangerouslySetInnerHTML={{ __html: decodeEntities(priceTag) }}
103 />
104 )}
105 {ctaType === 'plugin' && (
106 <InstallPluginAction
107 product={product}
108 ctaContent={decodeEntities(ctaContent)}
109 ctaPluginSlug={ctaPluginSlug}
110 />
111 )}
112 {ctaType === 'external-link' && (
113 <ExternalLinkAction
114 product={product}
115 ctaContent={decodeEntities(ctaContent)}
116 ctaExternalLink={ctaExternalLink}
117 />
118 )}
119 {ctaType === 'internal-link' && (
120 <InternalLinkAction
121 product={product}
122 ctaContent={decodeEntities(ctaContent)}
123 ctaInternalLink={ctaInternalLink}
124 />
125 )}
126 </div>
127 </div>
128 );
129 };
130
131 const InstallPluginAction = ({ product, ctaContent, ctaPluginSlug }) => {
132 const [status, setStatus] = useState('idle');
133 const [error, setError] = useState(null);
134
135 const handleInstall = useCallback(async () => {
136 recordActivity({
137 slot: 'plugin-search',
138 event: 'click-install',
139 product,
140 });
141
142 try {
143 setStatus('installing');
144 await Promise.all([
145 retryOperation(() => installPlugin(ctaPluginSlug), { maxAttempts: 2 }),
146 // Sleep makes sure the installing UI is displayed for at least 1 second.
147 sleep(1000),
148 ]);
149 } catch (_) {
150 setError(__('Failed to install the plugin', 'extendify-local'));
151 setStatus('error');
152 return;
153 }
154
155 recordPluginActivity({
156 slug: ctaPluginSlug,
157 source: 'search-recommendation-card',
158 });
159
160 try {
161 setStatus('activating');
162 await Promise.all([
163 retryOperation(() => activatePlugin(ctaPluginSlug), { maxAttempts: 2 }),
164 // Sleep makes sure the activating UI is displayed for at least 1 second.
165 sleep(1000),
166 ]);
167 } catch (_) {
168 setError(__('Failed to activate the plugin', 'extendify-local'));
169 setStatus('error');
170 return;
171 }
172
173 setStatus('activated');
174 }, [product, ctaPluginSlug]);
175
176 const actionText = {
177 idle: ctaContent,
178 installing: _x(
179 'Installing...',
180 'Plugin installation status',
181 'extendify-local',
182 ),
183 activating: _x(
184 'Activating...',
185 'Plugin activation status',
186 'extendify-local',
187 ),
188 activated: _x('Activated', 'Plugin activation status', 'extendify-local'),
189 error,
190 };
191
192 if (status === 'error') {
193 return (
194 <p className="m-0 flex items-center fill-wp-alert-red text-sm text-wp-alert-red">
195 <Icon icon={caution} />
196 {actionText[status]}
197 </p>
198 );
199 }
200
201 if (status === 'activated') {
202 return (
203 <p className="m-0 flex items-center fill-wp-alert-green text-sm text-wp-alert-green">
204 <Icon icon={check} />
205 {actionText[status]}
206 </p>
207 );
208 }
209
210 return (
211 <Button
212 className="min-w-24 whitespace-normal wrap-break-word rounded-xs bg-wp-theme-main px-3 align-middle text-sm text-design-text shadow-none hover:opacity-90 disabled:opacity-80"
213 type="button"
214 variant="secondary"
215 size="compact"
216 disabled={status !== 'idle'}
217 isBusy={status !== 'idle'}
218 onClick={handleInstall}
219 >
220 {actionText[status]}
221 </Button>
222 );
223 };
224
225 const ExternalLinkAction = ({ product, ctaContent, ctaExternalLink }) => {
226 const ctaExternalLinkWithPartner = decodeEntities(ctaExternalLink).replace(
227 '{PARTNERID}',
228 window.extSharedData?.partnerId,
229 );
230
231 return (
232 <a
233 onClick={() =>
234 recordActivity({
235 slot: 'plugin-search',
236 event: 'click-link-external',
237 product,
238 })
239 }
240 href={ctaExternalLinkWithPartner}
241 target="_blank"
242 className="relative flex min-h-8 min-w-24 cursor-pointer items-center justify-center whitespace-normal wrap-break-word rounded-xs bg-wp-theme-main fill-design-text py-1.5 pl-3 pr-9 text-center text-sm leading-tight text-design-text no-underline hover:opacity-90 focus:shadow-none"
243 >
244 {ctaContent}
245 <Icon className="absolute right-3 h-5 w-5" icon={external} />
246 </a>
247 );
248 };
249
250 const InternalLinkAction = ({ product, ctaContent, ctaInternalLink }) => {
251 return (
252 <a
253 onClick={() =>
254 recordActivity({
255 slot: 'plugin-search',
256 event: 'click-link-internal',
257 product,
258 })
259 }
260 href={ctaInternalLink}
261 className="relative flex min-h-8 min-w-24 cursor-pointer items-center justify-center whitespace-normal break-words rounded-xs bg-wp-theme-main fill-design-text px-3 py-[6px] text-center text-sm leading-tight text-design-text no-underline hover:opacity-90 focus:shadow-none"
262 >
263 {ctaContent}
264 </a>
265 );
266 };
267