PluginProbe
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO / 1.0.0
ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO v1.0.0
2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.1 2.1.0 2.0.2 2.0.1 2.0.0 1.32.0 1.31.0 1.30.0 1.29.0 1.28.0 1.27.0 1.26.0 1.25.0 trunk 1.0.0 1.0.1 1.0.2 1.1.0 1.10.0 All 48 releases
thinkrank / src / metabox / components / SocialTab.js

SocialTab.js in ThinkRank AI SEO – AI SEO Plugin for WordPress: Schema, XML Sitemaps, Meta Tags, Search Console & Local SEO 1.0.0, at src/metabox/components/SocialTab.js

321 lines 12.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Social Tab Component for Metabox
3 *
4 * Displays social media previews using data from SEO Analysis tab
5 * with fallbacks to post title and description. Automatically uses
6 * featured image when available.
7 *
8 * @package ThinkRank
9 * @since 1.0.0
10 */
11
12 import { __ } from '@wordpress/i18n';
13 import { useState, useEffect } from '@wordpress/element';
14 import {
15 Card,
16 CardHeader,
17 CardBody,
18 SelectControl,
19 Button,
20 Flex,
21 FlexItem,
22 Spinner
23 } from '@wordpress/components';
24 import apiFetch from '@wordpress/api-fetch';
25
26 /**
27 * Social Tab Component
28 */
29 const SocialTab = ({
30 postId,
31 postTitle = '',
32 formData = {},
33 showNotice
34 }) => {
35 const [selectedPlatform, setSelectedPlatform] = useState('facebook');
36 const [socialPreview, setSocialPreview] = useState(null);
37 const [isGenerating, setIsGenerating] = useState(false);
38 const [validationErrors, setValidationErrors] = useState([]);
39
40 // Get data from global metabox object
41 const metaboxData = window.thinkrankMetabox || {};
42 const featuredImageUrl = metaboxData.featuredImageUrl;
43 const postPermalink = metaboxData.postPermalink || '';
44 const siteName = metaboxData.siteName || '';
45
46 /**
47 * Get social media data with priority fallbacks
48 */
49 const getSocialData = () => {
50 // Priority 1: SEO Analysis data (AI-optimized)
51 const title = formData.thinkrank_seo_title ||
52 // Priority 2: Post title
53 postTitle ||
54 // Priority 3: Default
55 __('Your Post Title', 'thinkrank');
56
57 const description = formData.thinkrank_meta_description ||
58 // Priority 2: Default description
59 __('Add a meta description to improve how your post appears when shared on social media.', 'thinkrank');
60
61 return {
62 title: title,
63 description: description,
64 url: postPermalink,
65 image: featuredImageUrl || '',
66 type: 'article',
67 site_name: siteName
68 };
69 };
70
71 /**
72 * Validate social media data (basic validation only)
73 */
74 const validateSocialData = (data) => {
75 const errors = [];
76
77 // Only validate critical issues, not character limits
78 // Character limits are handled by backend truncation
79
80 // Image URL validation
81 if (data.image && !isValidImageUrl(data.image)) {
82 errors.push(__('Invalid image URL format', 'thinkrank'));
83 }
84
85 return { valid: errors.length === 0, errors };
86 };
87
88 /**
89 * Validate image URL format
90 */
91 const isValidImageUrl = (url) => {
92 if (!url || typeof url !== 'string') return false;
93
94 try {
95 const urlObj = new URL(url);
96 const validProtocols = ['http:', 'https:'];
97 const validExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
98
99 return validProtocols.includes(urlObj.protocol) &&
100 validExtensions.some(ext => urlObj.pathname.toLowerCase().endsWith(ext));
101 } catch {
102 return false;
103 }
104 };
105
106 /**
107 * Generate social media preview
108 */
109 const generatePreview = async () => {
110 try {
111 setIsGenerating(true);
112 setValidationErrors([]);
113
114 const socialData = getSocialData();
115
116 // Basic validation for critical issues only
117 const validation = validateSocialData(socialData);
118 if (!validation.valid) {
119 setValidationErrors(validation.errors);
120 showNotice(__('Please fix validation errors before generating preview', 'thinkrank'), 'error');
121 return;
122 }
123
124 const response = await apiFetch({
125 path: '/thinkrank/v1/social-media/preview',
126 method: 'POST',
127 data: {
128 data: socialData,
129 platform: selectedPlatform
130 }
131 });
132
133 if (response.success) {
134 setSocialPreview({
135 ...response.data,
136 platform: selectedPlatform
137 });
138 showNotice(__('Social media preview generated successfully!', 'thinkrank'), 'success');
139 } else {
140 showNotice(response.message || __('Failed to generate preview', 'thinkrank'), 'error');
141 }
142 } catch (error) {
143 // Fallback: Create a basic preview from available data
144 const socialData = getSocialData();
145 setSocialPreview({
146 title: socialData.title,
147 description: socialData.description,
148 image: socialData.image,
149 preview_url: socialData.url,
150 platform: selectedPlatform,
151 valid: true,
152 warnings: [__('Preview generated locally - API unavailable', 'thinkrank')]
153 });
154
155 showNotice(__('Generated basic preview (API unavailable)', 'thinkrank'), 'warning');
156 } finally {
157 setIsGenerating(false);
158 }
159 };
160
161 /**
162 * Auto-generate preview when platform changes or data updates
163 */
164 useEffect(() => {
165 if (postId && (formData.thinkrank_seo_title || postTitle)) {
166 // Only auto-generate if we have some content to work with
167 generatePreview();
168 }
169 }, [selectedPlatform, formData.thinkrank_seo_title, formData.thinkrank_meta_description]);
170
171 /**
172 * Render social media preview card
173 */
174 const renderPreviewCard = () => {
175 if (!socialPreview) {
176 return (
177 <div className="social-preview-placeholder">
178 <p>{__('Click "Generate Preview" to see how your post will appear on social media.', 'thinkrank')}</p>
179 </div>
180 );
181 }
182
183 const platformName = selectedPlatform.charAt(0).toUpperCase() + selectedPlatform.slice(1);
184
185 return (
186 <div className={`social-preview-card preview-${selectedPlatform}`}>
187 {/* Platform header */}
188 <div className="preview-platform-header">
189 <span className="preview-platform-name">
190 {platformName} {__('Preview', 'thinkrank')}
191 </span>
192 {socialPreview.valid ? (
193 <span className="preview-status preview-valid">{__('Valid', 'thinkrank')}</span>
194 ) : (
195 <span className="preview-status preview-invalid">{__('Issues Found', 'thinkrank')}</span>
196 )}
197 </div>
198
199 {/* Featured image */}
200 {socialPreview.image && (
201 <div className={`preview-image preview-image-${selectedPlatform}`}>
202 <img
203 src={socialPreview.image}
204 alt={socialPreview.title}
205 onError={(e) => {
206 e.target.style.display = 'none';
207 }}
208 />
209 </div>
210 )}
211
212 {/* Content */}
213 <div className="preview-content">
214 <h4 className="preview-title">{socialPreview.title}</h4>
215 <p className="preview-description">{socialPreview.description}</p>
216 <div className="preview-meta">
217 <span className="preview-url">{socialPreview.preview_url || postPermalink}</span>
218 {selectedPlatform === 'twitter' && (
219 <span className="preview-twitter-meta">
220 {socialPreview.title?.length || 0}/70 {__('chars', 'thinkrank')}
221 </span>
222 )}
223 </div>
224 </div>
225
226 {/* Validation messages */}
227 {socialPreview.warnings && socialPreview.warnings.length > 0 && (
228 <div className="preview-warnings">
229 <h5>{__('Warnings:', 'thinkrank')}</h5>
230 <ul>
231 {socialPreview.warnings.map((warning, index) => (
232 <li key={index}>{warning}</li>
233 ))}
234 </ul>
235 </div>
236 )}
237 </div>
238 );
239 };
240
241 return (
242 <div className="thinkrank-social-tab">
243 <Card>
244 <CardHeader>
245 <Flex justify="space-between" align="center">
246 <FlexItem>
247 <h3>{__('Social Media Preview', 'thinkrank')}</h3>
248 <p className="description">
249 {__('Preview how your post will appear when shared on social media platforms.', 'thinkrank')}
250 </p>
251 </FlexItem>
252 <FlexItem>
253 <Flex gap={2} align="center">
254 <SelectControl
255 value={selectedPlatform}
256 options={[
257 { label: __('Facebook', 'thinkrank'), value: 'facebook' },
258 { label: __('Twitter', 'thinkrank'), value: 'twitter' },
259 { label: __('LinkedIn', 'thinkrank'), value: 'linkedin' },
260 { label: __('Pinterest', 'thinkrank'), value: 'pinterest' }
261 ]}
262 onChange={(value) => setSelectedPlatform(value)}
263 __next40pxDefaultSize={true}
264 __nextHasNoMarginBottom={true}
265 />
266 <Button
267 variant="primary"
268 onClick={generatePreview}
269 isBusy={isGenerating}
270 disabled={isGenerating}
271 >
272 {isGenerating ? __('Generating...', 'thinkrank') : __('Generate Preview', 'thinkrank')}
273 </Button>
274 </Flex>
275 </FlexItem>
276 </Flex>
277 </CardHeader>
278 <CardBody>
279 {/* Preview card */}
280 <div className="social-preview-container">
281 {renderPreviewCard()}
282 </div>
283
284 {/* Data source info */}
285 <div className="data-source-info">
286 <h4>{__('Data Sources', 'thinkrank')}</h4>
287 <ul>
288 <li>
289 <strong>{__('Title:', 'thinkrank')}</strong> {
290 formData.thinkrank_seo_title ?
291 __('Using SEO Analysis title', 'thinkrank') :
292 __('Using post title', 'thinkrank')
293 }
294 </li>
295 <li>
296 <strong>{__('Description:', 'thinkrank')}</strong> {
297 formData.thinkrank_meta_description ?
298 __('Using SEO Analysis description', 'thinkrank') :
299 __('Using default description', 'thinkrank')
300 }
301 </li>
302 <li>
303 <strong>{__('Image:', 'thinkrank')}</strong> {
304 featuredImageUrl ?
305 __('Using featured image', 'thinkrank') :
306 __('No featured image set', 'thinkrank')
307 }
308 </li>
309 </ul>
310 <p className="help-text">
311 {__('Tip: Generate SEO metadata in the SEO Analysis tab for optimized social media sharing.', 'thinkrank')}
312 </p>
313 </div>
314 </CardBody>
315 </Card>
316 </div>
317 );
318 };
319
320 export default SocialTab;
321