PluginProbe
Extendify / 3.0.6
Extendify v3.0.6
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.0.6, at src/Recommendations/components/RecommendationCard.jsx

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