WebOrbiton
v3.0.0.0

FlatlyPage

1,443 lines · 77.0 KB
  1. <?php
  2. ​
  3. if (!defined('BASE_DIR')) {
  4. die('Direct access not permitted');
  5. }
  6. ​
  7. final class RenderCache
  8. {
  9. private static $cache = [];
  10. private static $validated = [];
  11. private static $hits = 0;
  12. private static $misses = 0;
  13. private const MAX_CACHE_SIZE = 100;
  14. private const MAX_KEY_LENGTH = 128;
  15. private const MAX_VALUE_SIZE = 102400;
  16. ​
  17. private static function generateSecureKey($data)
  18. {
  19. $serialized = serialize($data);
  20. return hash('sha256', $serialized);
  21. }
  22. ​
  23. ​
  24. private static function isValidCachedContent($value)
  25. {
  26. if (!is_string($value)) {
  27. return false;
  28. }
  29. ​
  30. if (strlen($value) > self::MAX_VALUE_SIZE) {
  31. return false;
  32. }
  33. ​
  34. $dangerous_patterns = [
  35. '/<script[^>]*>.*?<\/script>/is',
  36. '/javascript:/i',
  37. '/on\w+\s*=/i',
  38. '/<iframe[^>]*srcdoc/i',
  39. ];
  40. ​
  41. foreach ($dangerous_patterns as $pattern) {
  42. if (preg_match($pattern, $value)) {
  43. return false;
  44. }
  45. }
  46. ​
  47. return true;
  48. }
  49. ​
  50. public static function get($key)
  51. {
  52. $secure_key = self::generateSecureKey($key);
  53. ​
  54. if (isset(self::$cache[$secure_key])) {
  55. $value = self::$cache[$secure_key];
  56. ​
  57. if (isset(self::$validated[$secure_key]) || self::isValidCachedContent($value)) {
  58. self::$validated[$secure_key] = true;
  59. self::$hits++;
  60. return $value;
  61. } else {
  62. unset(self::$cache[$secure_key], self::$validated[$secure_key]);
  63. self::$misses++;
  64. return null;
  65. }
  66. }
  67. ​
  68. self::$misses++;
  69. return null;
  70. }
  71. ​
  72. public static function set($key, $value)
  73. {
  74. if (!is_string($value) || strlen($value) > self::MAX_VALUE_SIZE) {
  75. return false;
  76. }
  77. ​
  78. $secure_key = self::generateSecureKey($key);
  79. ​
  80. if (count(self::$cache) >= self::MAX_CACHE_SIZE) {
  81. array_shift(self::$cache);
  82. }
  83. ​
  84. unset(self::$validated[$secure_key]);
  85. self::$cache[$secure_key] = $value;
  86. return true;
  87. }
  88. ​
  89. public static function clear()
  90. {
  91. self::$cache = [];
  92. self::$validated = [];
  93. self::$hits = 0;
  94. self::$misses = 0;
  95. }
  96. ​
  97. public static function getStats()
  98. {
  99. return [
  100. 'hits' => self::$hits,
  101. 'misses' => self::$misses,
  102. 'size' => count(self::$cache)
  103. ];
  104. }
  105. }
  106. ​
  107. ​
  108. final class LazyLoadManager
  109. {
  110. private static $lazy_blocks = [];
  111. private static $script_added = false;
  112. private const MAX_LAZY_BLOCKS = 50;
  113. private const MAX_BLOCK_SIZE = 51200; // 50KB per block
  114. ​
  115. private static function generateSecureBlockId($block)
  116. {
  117. $data = serialize($block);
  118. return 'block_' . hash('sha256', $data . random_bytes(16));
  119. }
  120. ​
  121. private static function sanitizeLazyHTML($html)
  122. {
  123. if (!is_string($html)) {
  124. return '';
  125. }
  126. ​
  127. if (strlen($html) > self::MAX_BLOCK_SIZE) {
  128. return '';
  129. }
  130. ​
  131. $html = preg_replace('/<script[^>]*>.*?<\/script>/is', '', $html);
  132. ​
  133. $html = preg_replace('/javascript:/i', '', $html);
  134. ​
  135. $html = preg_replace('/\s*on\w+\s*=\s*["\'][^"\']*["\']/i', '', $html);
  136. ​
  137. return $html;
  138. }
  139. ​
  140. public static function registerLazyBlock($block_data, $rendered_html)
  141. {
  142. if (count(self::$lazy_blocks) >= self::MAX_LAZY_BLOCKS) {
  143. return null;
  144. }
  145. ​
  146. $sanitized_html = self::sanitizeLazyHTML($rendered_html);
  147. if (empty($sanitized_html)) {
  148. return null;
  149. }
  150. ​
  151. $block_id = self::generateSecureBlockId($block_data);
  152. self::$lazy_blocks[$block_id] = $sanitized_html;
  153. ​
  154. return $block_id;
  155. }
  156. ​
  157. public static function getLazyPlaceholder($block_id, $type)
  158. {
  159. if (!preg_match('/^block_[a-f0-9]{64}$/', $block_id)) {
  160. return '';
  161. }
  162. ​
  163. $min_height = self::getMinHeightForType($type);
  164. ​
  165. $safe_block_id = htmlspecialchars($block_id, ENT_QUOTES, 'UTF-8');
  166. ​
  167. return sprintf(
  168. '<div class="lazy-block" data-block-id="%s" style="min-height:%dpx">
  169. <div class="lazy-placeholder">
  170. <div class="lazy-spinner"></div>
  171. </div>
  172. </div>',
  173. $safe_block_id,
  174. (int)$min_height
  175. );
  176. }
  177. ​
  178. private static function getMinHeightForType($type)
  179. {
  180. $heights = [
  181. 'hero' => 500,
  182. 'features' => 400,
  183. 'testimonials' => 300,
  184. 'pricing' => 400,
  185. 'gallery' => 300,
  186. 'team' => 350,
  187. 'video' => 400,
  188. 'default' => 200
  189. ];
  190. ​
  191. return $heights[$type] ?? $heights['default'];
  192. }
  193. ​
  194. public static function outputLazyScript($nonce = '')
  195. {
  196. if (self::$script_added || empty(self::$lazy_blocks)) {
  197. return '';
  198. }
  199. ​
  200. self::$script_added = true;
  201. ​
  202. $blocks_json = json_encode(self::$lazy_blocks, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
  203. ​
  204. $nonce_attr = $nonce ? ' nonce="' . htmlspecialchars($nonce, ENT_QUOTES, 'UTF-8') . '"' : '';
  205. ​
  206. return <<<HTML
  207. <script{$nonce_attr}>
  208. (function() {
  209. 'use strict';
  210. ​
  211. let lazyBlocks;
  212. try {
  213. lazyBlocks = {$blocks_json};
  214. } catch (e) {
  215. console.error('Failed to parse lazy blocks:', e);
  216. return;
  217. }
  218. ​
  219. const validBlockIdPattern = /^block_[a-f0-9]{64}$/;
  220. ​
  221. const observer = new IntersectionObserver((entries) => {
  222. entries.forEach(entry => {
  223. if (entry.isIntersecting) {
  224. const element = entry.target;
  225. const blockId = element.getAttribute('data-block-id');
  226. ​
  227. if (!blockId || !validBlockIdPattern.test(blockId)) {
  228. console.warn('Invalid block ID detected');
  229. observer.unobserve(element);
  230. return;
  231. }
  232. ​
  233. if (lazyBlocks[blockId]) {
  234. const temp = document.createElement('div');
  235. temp.innerHTML = lazyBlocks[blockId];
  236. ​
  237. temp.querySelectorAll('script').forEach(s => s.remove());
  238. ​
  239. element.innerHTML = temp.innerHTML;
  240. observer.unobserve(element);
  241. delete lazyBlocks[blockId];
  242. ​
  243. element.dispatchEvent(new CustomEvent('blockLoaded', {
  244. detail: { blockId: blockId }
  245. }));
  246. }
  247. }
  248. });
  249. }, {
  250. rootMargin: '200px',
  251. threshold: 0.01
  252. });
  253. ​
  254. document.querySelectorAll('.lazy-block').forEach(block => {
  255. const blockId = block.getAttribute('data-block-id');
  256. ​
  257. if (blockId && validBlockIdPattern.test(blockId)) {
  258. observer.observe(block);
  259. }
  260. });
  261. ​
  262. if (!('IntersectionObserver' in window)) {
  263. document.querySelectorAll('.lazy-block').forEach(block => {
  264. const blockId = block.getAttribute('data-block-id');
  265. ​
  266. if (blockId && validBlockIdPattern.test(blockId) && lazyBlocks[blockId]) {
  267. const temp = document.createElement('div');
  268. temp.innerHTML = lazyBlocks[blockId];
  269. temp.querySelectorAll('script').forEach(s => s.remove());
  270. block.innerHTML = temp.innerHTML;
  271. }
  272. });
  273. }
  274. })();
  275. </script>
  276. HTML;
  277. }
  278. ​
  279. public static function clear()
  280. {
  281. self::$lazy_blocks = [];
  282. self::$script_added = false;
  283. }
  284. }
  285. ​
  286. ​
  287. final class IconRegistry
  288. {
  289. private static $used_icons = [];
  290. private static $used_social_icons = [];
  291. private static $custom_icons = [];
  292. private static $custom_social = [];
  293. private const MAX_TRACKED = 100;
  294. ​
  295. private const ALLOWED_ICONS = [
  296. 'bolt',
  297. 'layout',
  298. 'clock',
  299. 'shield',
  300. 'chart',
  301. 'code',
  302. 'star',
  303. 'heart',
  304. 'globe',
  305. 'users',
  306. 'settings',
  307. 'edit',
  308. 'plus',
  309. 'trash',
  310. 'copy',
  311. 'eye',
  312. 'eyeOff',
  313. 'image',
  314. 'file',
  315. 'upload',
  316. 'money'
  317. ];
  318. ​
  319. private const ALLOWED_SOCIAL = [
  320. 'twitter',
  321. 'github',
  322. 'linkedin',
  323. 'facebook',
  324. 'instagram',
  325. 'youtube',
  326. 'tiktok',
  327. 'pinterest',
  328. 'reddit',
  329. 'discord',
  330. 'snapchat',
  331. 'whatsapp',
  332. 'telegram',
  333. 'twitch',
  334. 'spotify',
  335. 'medium',
  336. 'slack',
  337. 'dribbble',
  338. 'mastodon',
  339. 'patreon',
  340. 'kofi',
  341. 'vimeo',
  342. 'tumblr',
  343. 'stackoverflow',
  344. 'gitlab',
  345. 'bluesky',
  346. 'line',
  347. 'wechat',
  348. 'flatlypage',
  349. 'xing'
  350. ];
  351. ​
  352. public static function registerIcon($name, $svg)
  353. {
  354. if (count(self::$custom_icons) >= 50) {
  355. return false;
  356. }
  357. ​
  358. if (!preg_match('/^[a-z0-9_-]+$/', $name)) {
  359. return false;
  360. }
  361. ​
  362. if (!is_string($svg) || strlen($svg) > 5000) {
  363. return false;
  364. }
  365. ​
  366. self::$custom_icons[$name] = $svg;
  367. return true;
  368. }
  369. ​
  370. public static function registerSocialIcon($platform, $svg)
  371. {
  372. if (count(self::$custom_social) >= 20) {
  373. return false;
  374. }
  375. ​
  376. if (!preg_match('/^[a-z0-9_-]+$/', $platform)) {
  377. return false;
  378. }
  379. ​
  380. if (!is_string($svg) || strlen($svg) > 5000) {
  381. return false;
  382. }
  383. ​
  384. self::$custom_social[$platform] = $svg;
  385. return true;
  386. }
  387. ​
  388. public static function getCustomIcon($name)
  389. {
  390. return self::$custom_icons[$name] ?? null;
  391. }
  392. ​
  393. public static function getCustomSocial($platform)
  394. {
  395. return self::$custom_social[$platform] ?? null;
  396. }
  397. ​
  398. public static function markIconUsed($icon)
  399. {
  400. if (count(self::$used_icons) >= self::MAX_TRACKED) {
  401. return false;
  402. }
  403. ​
  404. if (!in_array($icon, self::ALLOWED_ICONS, true) && !isset(self::$custom_icons[$icon])) {
  405. return false;
  406. }
  407. ​
  408. self::$used_icons[$icon] = true;
  409. return true;
  410. }
  411. ​
  412. public static function markSocialUsed($platform)
  413. {
  414. if (count(self::$used_social_icons) >= self::MAX_TRACKED) {
  415. return false;
  416. }
  417. ​
  418. if (!in_array($platform, self::ALLOWED_SOCIAL, true) && !isset(self::$custom_social[$platform])) {
  419. return false;
  420. }
  421. ​
  422. self::$used_social_icons[$platform] = true;
  423. return true;
  424. }
  425. ​
  426. public static function isIconAllowed($icon)
  427. {
  428. return in_array($icon, self::ALLOWED_ICONS, true) || isset(self::$custom_icons[$icon]);
  429. }
  430. ​
  431. public static function isSocialAllowed($platform)
  432. {
  433. return in_array($platform, self::ALLOWED_SOCIAL, true) || isset(self::$custom_social[$platform]);
  434. }
  435. }
  436. ​
  437. ​
  438. function get_icon_svg($icon)
  439. {
  440. $custom = IconRegistry::getCustomIcon($icon);
  441. if ($custom !== null) {
  442. return $custom;
  443. }
  444. ​
  445. if (!IconRegistry::isIconAllowed($icon)) {
  446. $icon = 'bolt';
  447. }
  448. ​
  449. static $icons = [
  450. 'bolt' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>',
  451. 'layout' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18M9 21V9"/></svg>',
  452. 'clock' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>',
  453. 'shield' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
  454. 'chart' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/></svg>',
  455. 'code' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
  456. 'star' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>',
  457. 'heart' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>',
  458. 'globe' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
  459. 'users' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
  460. 'settings' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>',
  461. 'edit' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>',
  462. 'plus' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>',
  463. 'trash' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>',
  464. 'copy' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>',
  465. 'eye' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>',
  466. 'eyeOff' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94"/><path d="M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19"/><line x1="1" y1="1" x2="23" y2="23"/></svg>',
  467. 'image' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg>',
  468. 'file' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg>',
  469. 'upload' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>',
  470. 'money' => '<svg fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/></svg>',
  471. ];
  472. ​
  473. return $icons[$icon];
  474. }
  475. ​
  476. function render_icon($icon)
  477. {
  478. $icon = preg_replace('/[^a-z_-]/', '', strtolower($icon));
  479. ​
  480. if (!IconRegistry::markIconUsed($icon)) {
  481. $icon = 'bolt';
  482. }
  483. ​
  484. return get_icon_svg($icon);
  485. }
  486. ​
  487. function get_social_icon_svg($platform)
  488. {
  489. $custom = IconRegistry::getCustomSocial($platform);
  490. if ($custom !== null) {
  491. return $custom;
  492. }
  493. ​
  494. if (!IconRegistry::isSocialAllowed($platform)) {
  495. $platform = 'twitter';
  496. }
  497. ​
  498. static $icons = [
  499. 'twitter' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>',
  500. 'github' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.477 2 12c0 4.42 2.865 8.17 6.839 9.49.5.092.682-.217.682-.482 0-.237-.008-.866-.013-1.7-2.782.603-3.369-1.34-3.369-1.34-.454-1.156-1.11-1.463-1.11-1.463-.908-.62.069-.608.069-.608 1.003.07 1.531 1.03 1.531 1.03.892 1.529 2.341 1.087 2.91.831.092-.646.35-1.086.636-1.336-2.22-.253-4.555-1.11-4.555-4.943 0-1.091.39-1.984 1.029-2.683-.103-.253-.446-1.27.098-2.647 0 0 .84-.269 2.75 1.025A9.578 9.578 0 0112 6.836c.85.004 1.705.114 2.504.336 1.909-1.294 2.747-1.025 2.747-1.025.546 1.377.203 2.394.1 2.647.64.699 1.028 1.592 1.028 2.683 0 3.842-2.339 4.687-4.566 4.935.359.309.678.919.678 1.852 0 1.336-.012 2.415-.012 2.743 0 .267.18.578.688.48C19.138 20.167 22 16.418 22 12c0-5.523-4.477-10-10-10z"/></svg>',
  501. 'linkedin' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>',
  502. 'facebook' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z"/></svg>',
  503. 'instagram' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z"/></svg>',
  504. 'youtube' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>',
  505. 'tiktok' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></svg>',
  506. 'pinterest' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12.017 0C5.396 0 .029 5.367.029 11.987c0 5.079 3.158 9.417 7.618 11.162-.105-.949-.199-2.403.041-3.439.219-.937 1.406-5.957 1.406-5.957s-.359-.72-.359-1.781c0-1.663.967-2.911 2.168-2.911 1.024 0 1.518.769 1.518 1.688 0 1.029-.653 2.567-.992 3.992-.285 1.193.6 2.165 1.775 2.165 2.128 0 3.768-2.245 3.768-5.487 0-2.861-2.063-4.869-5.008-4.869-3.41 0-5.409 2.562-5.409 5.199 0 1.033.394 2.143.889 2.741.099.12.112.225.085.345-.09.375-.293 1.199-.334 1.363-.053.225-.172.271-.401.165-1.495-.69-2.433-2.878-2.433-4.646 0-3.776 2.748-7.252 7.92-7.252 4.158 0 7.392 2.967 7.392 6.923 0 4.135-2.607 7.462-6.233 7.462-1.225 0-2.376-.637-2.771-1.388l-.753 2.87c-.271 1.048-1.007 2.361-1.497 3.161C9.142 23.812 10.544 24 12.017 24c6.627 0 11.999-5.373 11.999-11.987C24.016 5.367 18.644 0 12.017 0z"/></svg>',
  507. 'reddit' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/></svg>',
  508. 'discord' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>',
  509. 'snapchat' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12.206.793c.99 0 4.347.276 5.93 3.821.529 1.193.403 3.219.299 4.847l-.003.06c-.012.18-.022.345-.03.51.075.045.203.09.401.09.3-.016.659-.12 1.033-.301.165-.088.344-.104.464-.104.182 0 .359.029.509.09.45.149.734.479.734.838.015.449-.39.839-1.213 1.168-.089.029-.209.075-.344.119-.45.135-1.139.36-1.333.81-.09.224-.061.524.12.868l.015.015c.06.136 1.526 3.475 4.791 4.014.255.044.435.27.42.509 0 .075-.015.149-.045.225-.24.569-1.273.988-3.146 1.271-.059.091-.12.375-.164.57-.029.179-.074.36-.134.553-.076.271-.27.405-.555.405h-.03c-.135 0-.313-.031-.538-.074-.36-.075-.765-.135-1.273-.135-.3 0-.599.015-.913.074-.6.104-1.123.464-1.723.884-.853.599-1.826 1.288-3.294 1.288-.06 0-.119-.015-.18-.015h-.149c-1.468 0-2.427-.675-3.279-1.288-.599-.42-1.107-.779-1.707-.884-.314-.045-.629-.074-.928-.074-.54 0-.958.089-1.272.149-.211.043-.391.074-.54.074-.374 0-.523-.224-.583-.42-.061-.192-.09-.389-.135-.567-.046-.181-.105-.494-.166-.57-1.918-.222-2.95-.642-3.189-1.226-.031-.063-.052-.15-.055-.225-.015-.243.165-.465.42-.509 3.264-.54 4.73-3.879 4.791-4.02l.016-.029c.18-.345.224-.645.119-.869-.195-.434-.884-.658-1.332-.809-.121-.029-.24-.074-.346-.119-1.107-.435-1.257-.93-1.197-1.273.09-.479.674-.793 1.168-.793.146 0 .27.029.383.074.42.194.789.3 1.104.3.234 0 .384-.06.465-.105l-.046-.569c-.098-1.626-.225-3.651.307-4.837C7.392 1.077 10.739.807 11.727.807l.419-.015h.06z"/></svg>',
  510. 'whatsapp' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413z"/></svg>',
  511. 'telegram' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>',
  512. 'twitch' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"/></svg>',
  513. 'spotify' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>',
  514. 'medium' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M13.54 12a6.8 6.8 0 01-6.77 6.82A6.8 6.8 0 010 12a6.8 6.8 0 016.77-6.82A6.8 6.8 0 0113.54 12zM20.96 12c0 3.54-1.51 6.42-3.38 6.42-1.87 0-3.39-2.88-3.39-6.42s1.52-6.42 3.39-6.42 3.38 2.88 3.38 6.42M24 12c0 3.17-.53 5.75-1.19 5.75-.66 0-1.19-2.58-1.19-5.75s.53-5.75 1.19-5.75C23.47 6.25 24 8.83 24 12z"/></svg>',
  515. 'slack' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/></svg>',
  516. 'dribbble' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 24C5.385 24 0 18.615 0 12S5.385 0 12 0s12 5.385 12 12-5.385 12-12 12zm10.12-10.358c-.35-.11-3.17-.953-6.384-.438 1.34 3.684 1.887 6.684 1.992 7.308 2.3-1.555 3.936-4.02 4.395-6.87zm-6.115 7.808c-.153-.9-.75-4.032-2.19-7.77l-.066.02c-5.79 2.015-7.86 6.025-8.04 6.4 1.73 1.358 3.92 2.166 6.29 2.166 1.42 0 2.77-.29 4-.814zm-11.62-2.58c.232-.4 3.045-5.055 8.332-6.765.135-.045.27-.084.405-.12-.26-.585-.54-1.167-.832-1.74C7.17 11.775 2.206 11.71 1.756 11.7l-.004.312c0 2.633.998 5.037 2.634 6.855zm-2.42-8.955c.46.008 4.683.026 9.477-1.248-1.698-3.018-3.53-5.558-3.8-5.928-2.868 1.35-5.01 3.99-5.676 7.17zM9.6 2.052c.282.38 2.145 2.914 3.822 6 3.645-1.365 5.19-3.44 5.373-3.702-1.81-1.61-4.19-2.586-6.795-2.586-.825 0-1.63.1-2.4.285zm10.335 3.483c-.218.29-1.935 2.493-5.724 4.04.24.49.47.985.68 1.486.08.18.15.36.22.53 3.41-.43 6.8.26 7.14.33-.02-2.42-.88-4.64-2.31-6.38z"/></svg>',
  517. 'mastodon' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>',
  518. 'patreon' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.386.524c-4.764 0-8.64 3.876-8.64 8.64 0 4.75 3.876 8.613 8.64 8.613 4.75 0 8.614-3.864 8.614-8.613C24 4.4 20.136.524 15.386.524M.003 23.537h4.22V.524H.003"/></svg>',
  519. 'kofi' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.881 8.948c-.773-4.085-4.859-4.593-4.859-4.593H.723c-.604 0-.679.798-.679.798s-.082 7.324-.022 11.822c.164 2.424 2.586 2.672 2.586 2.672s8.267-.023 11.966-.049c2.438-.426 2.683-2.566 2.658-3.734 4.352.24 7.422-2.831 6.649-6.916zm-11.062 3.511c-1.246 1.453-4.011 3.976-4.011 3.976s-.121.119-.31.023c-.076-.057-.108-.09-.108-.09-.443-.441-3.368-3.049-4.034-3.954-.709-.965-1.041-2.7-.091-3.71.951-1.01 3.005-1.086 4.363.407 0 0 1.565-1.782 3.468-.963 1.904.82 1.832 3.011.723 4.311zm6.173.478c-.928.116-1.682.028-1.682.028V7.284h1.77s1.971.551 1.971 2.638c0 1.913-.985 2.667-2.059 3.015z"/></svg>',
  520. 'vimeo' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.977 6.416c-.105 2.338-1.739 5.543-4.894 9.609-3.268 4.247-6.026 6.37-8.29 6.37-1.409 0-2.578-1.294-3.553-3.881L5.322 11.4C4.603 8.816 3.834 7.522 3.01 7.522c-.179 0-.806.378-1.881 1.132L0 7.197c1.185-1.044 2.351-2.084 3.501-3.128C5.08 2.701 6.266 1.984 7.055 1.91c1.867-.18 3.016 1.1 3.447 3.838.465 2.953.789 4.789.971 5.507.539 2.45 1.131 3.674 1.776 3.674.502 0 1.256-.796 2.265-2.385 1.004-1.589 1.54-2.797 1.612-3.628.144-1.371-.395-2.061-1.614-2.061-.574 0-1.167.121-1.777.391 1.186-3.868 3.434-5.757 6.762-5.637 2.473.06 3.628 1.664 3.493 4.797z"/></svg>',
  521. 'tumblr' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M14.563 24c-5.093 0-7.031-3.756-7.031-6.411V9.747H5.116V6.648c3.63-1.313 4.512-4.596 4.71-6.469C9.84.051 9.941 0 9.999 0h3.517v6.114h4.801v3.633h-4.82v7.47c.016 1.001.375 2.371 2.207 2.371h.09c.631-.02 1.486-.205 1.936-.419l1.156 3.425c-.436.636-2.4 1.374-4.156 1.404h-.178l.011.002z"/></svg>',
  522. 'stackoverflow' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.725 0l-1.72 1.277 6.39 8.588 1.716-1.277L15.725 0zm-3.94 3.418l-1.369 1.644 8.225 6.85 1.369-1.644-8.225-6.85zm-3.15 4.465l-.905 1.94 9.702 4.517.904-1.94-9.701-4.517zm-1.85 4.86l-.44 2.093 10.473 2.201.44-2.092-10.473-2.203zM1.89 15.47V24h19.19v-8.53h-2.133v6.397H4.021v-6.396H1.89zm4.265 2.133v2.13h10.66v-2.13H6.154Z"/></svg>',
  523. 'gitlab' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.955 13.587l-1.342-4.135-2.664-8.189a.455.455 0 0 0-.867 0L16.418 9.45H7.582L4.919 1.263C4.783.84 4.185.84 4.05 1.26L1.386 9.449.044 13.587a.924.924 0 0 0 .331 1.023L12 23.054l11.625-8.443a.92.92 0 0 0 .330-1.024"/></svg>',
  524. 'bluesky' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8z"/></svg>',
  525. 'line' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.365 9.863c.349 0 .63.285.63.631 0 .345-.281.63-.63.63H17.61v1.125h1.755c.349 0 .63.283.63.63 0 .344-.281.629-.63.629h-2.386c-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63h2.386c.346 0 .627.285.627.63 0 .349-.281.63-.63.63H17.61v1.125h1.755zm-3.855 3.016c0 .27-.174.51-.432.596-.064.021-.133.031-.199.031-.211 0-.391-.09-.51-.25l-2.443-3.317v2.94c0 .344-.279.629-.631.629-.346 0-.626-.285-.626-.629V8.108c0-.27.173-.51.43-.595.06-.023.136-.033.194-.033.195 0 .375.105.495.254l2.462 3.33V8.108c0-.345.282-.63.63-.63.345 0 .63.285.63.63v4.771zm-5.741 0c0 .344-.282.629-.631.629-.345 0-.627-.285-.627-.629V8.108c0-.345.282-.63.63-.63.346 0 .628.285.628.63v4.771zm-2.466.629H4.917c-.345 0-.63-.285-.63-.629V8.108c0-.345.285-.63.63-.63.348 0 .63.285.63.63v4.141h1.756c.348 0 .629.283.629.63 0 .344-.282.629-.629.629M24 10.314C24 4.943 18.615.572 12 .572S0 4.943 0 10.314c0 4.811 4.27 8.842 10.035 9.608.391.082.923.258 1.058.59.12.301.079.766.038 1.08l-.164 1.02c-.045.301-.24 1.186 1.049.645 1.291-.539 6.916-4.078 9.436-6.975C23.176 14.393 24 12.458 24 10.314"/></svg>',
  526. 'wechat' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.027zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/></svg>',
  527. 'flatlypage' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M1.139,10.785c2.355,-2.34 2.35,-2.344 2.556,-2.547c0.078,-0.077 3.035,-0.02 4.087,-0.036c0.256,-0.004 1.694,-2.812 5.62,-5.095c5.564,-3.236 9.601,-2.389 10.558,-2.232c0.071,0.012 0.043,0.088 -0.059,1.504c-0.647,8.983 -7.685,12.863 -8.987,13.72c-0.197,0.13 0.229,2.523 -0.192,3.517c-0.419,0.99 -3.908,3.683 -4.235,3.723c-0.417,0.051 -0.01,-2.035 -0.712,-3.864c-0.935,-2.437 -3.147,-1.942 -2.359,-3.122c0.131,-0.196 4.425,-3.782 5.035,-4.322c0.579,-0.513 0.008,-1.327 -0.6,-1.113c-0.322,0.114 -5.417,4.593 -5.553,4.647c-0.553,0.22 -0.839,-0.788 -0.974,-1.096c-0.816,-1.857 -3.157,-1.629 -4.786,-1.905c-1.026,-0.174 -0.451,-0.695 0.6,-1.78l17.729,-5.714c-0.291,-0.219 -0.274,-0.252 -0.625,-0.354c-1.886,-0.551 -2.944,2.007 -1.401,2.989c1.707,1.086 3.541,-1.228 2.026,-2.635l-17.729,5.714Z"/><path d="M7.464,20.241c-1.992,2.204 -4.202,1.853 -6.596,1.997c-0.097,0.006 -0.039,-1.98 0.059,-2.738c0.054,-0.421 0.244,-2.487 1.925,-3.848c1.683,-1.362 2.31,0.71 2.182,0.75c-2.12,0.665 -2.297,2.159 -2.629,3.897c-0.045,0.235 -0.08,0.321 0.157,0.341c0.161,0.014 1.862,-0.167 2.838,-0.777c1.305,-0.815 1.275,-1.923 1.418,-1.897c0.095,0.017 0.695,0.126 1.026,0.644c0.456,0.714 -0.364,1.61 -0.38,1.631Z"/></svg>',
  528. 'xing' => '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.188 0c-.517 0-.741.325-.927.66 0 0-7.455 13.224-7.702 13.657.015.024 4.919 9.023 4.919 9.023.17.308.436.66.967.66h3.454c.211 0 .375-.078.463-.22.089-.151.089-.346-.009-.536l-4.879-8.916c-.004-.006-.004-.016 0-.022L22.139.756c.095-.191.097-.387.006-.535C22.056.078 21.894 0 21.686 0h-3.498zM3.648 4.74c-.211 0-.385.074-.473.216-.09.149-.078.339.02.531l2.34 4.05c.004.01.004.016 0 .021L1.86 16.051c-.099.188-.093.381 0 .529.085.142.239.234.45.234h3.461c.518 0 .766-.348.945-.667l3.734-6.609-2.378-4.155c-.172-.315-.434-.659-.962-.659H3.648v.016z"/></svg>',
  529. ];
  530. ​
  531. return $icons[$platform];
  532. }
  533. ​
  534. function render_social_icon($platform)
  535. {
  536. $platform = preg_replace('/[^a-z_-]/', '', strtolower($platform));
  537. ​
  538. if (!IconRegistry::markSocialUsed($platform)) {
  539. $platform = 'twitter';
  540. }
  541. ​
  542. return get_social_icon_svg($platform);
  543. }
  544. ​
  545. ​
  546. function apply_bbcode($html)
  547. {
  548. $html = (string) $html;
  549. if (strpos($html, '[') === false) {
  550. return $html;
  551. }
  552. ​
  553. $tags = [];
  554. $masked = preg_replace_callback('/<[^>]*>/', static function ($match) use (&$tags) {
  555. $tags[] = $match[0];
  556. return "\x01" . (count($tags) - 1) . "\x02";
  557. }, $html);
  558. ​
  559. if ($masked === null) {
  560. return $html;
  561. }
  562. ​
  563. $masked = preg_replace([
  564. '/\[u\](.*?)\[\/u\]/is',
  565. '/\[b\](.*?)\[\/b\]/is',
  566. '/\[i\](.*?)\[\/i\]/is',
  567. '/\[s\](.*?)\[\/s\]/is',
  568. '/\[h\](.*?)\[\/h\]/is',
  569. '/\[quote\](.*?)\[\/quote\]/is',
  570. ], [
  571. '<u>$1</u>',
  572. '<b>$1</b>',
  573. '<i>$1</i>',
  574. '<s>$1</s>',
  575. '<h3>$1</h3>',
  576. '<blockquote>$1</blockquote>',
  577. ], $masked);
  578. ​
  579. if ($masked === null) {
  580. return $html;
  581. }
  582. ​
  583. return preg_replace_callback('/\x01(\d+)\x02/', static function ($match) use ($tags) {
  584. return $tags[(int) $match[1]] ?? '';
  585. }, $masked);
  586. }
  587. ​
  588. function render_block($block, $options = [])
  589. {
  590. if (!is_array($block)) {
  591. return '';
  592. }
  593. ​
  594. $type = $block['type'] ?? '';
  595. $data = $block['data'] ?? [];
  596. ​
  597. $cache_key = ['type' => $type, 'data' => $data, 'id' => $block['id'] ?? ''];
  598. ​
  599. $cached = RenderCache::get($cache_key);
  600. if ($cached !== null) {
  601. return $cached;
  602. }
  603. ​
  604. $id = '';
  605. if (!empty($block['id'])) {
  606. $clean_id = preg_replace('/[^a-zA-Z0-9_-]/', '', $block['id']);
  607. $id = ' id="' . e($clean_id) . '"';
  608. }
  609. ​
  610. $allowed_blocks = [
  611. 'hero',
  612. 'blog-hero',
  613. 'stats',
  614. 'features',
  615. 'testimonials',
  616. 'pricing',
  617. 'cta',
  618. 'text',
  619. 'image',
  620. 'image-text',
  621. 'product-cards',
  622. 'video',
  623. 'gallery',
  624. 'faq',
  625. 'team',
  626. 'audio',
  627. 'countdown',
  628. 'newsletter',
  629. 'html'
  630. ];
  631. ​
  632. if (!in_array($type, $allowed_blocks, true)) {
  633. return '';
  634. }
  635. ​
  636. $lazy_types = ['gallery', 'team', 'testimonials', 'video', 'audio'];
  637. $should_lazy_load = isset($options['enable_lazy']) && $options['enable_lazy']
  638. && in_array($type, $lazy_types, true);
  639. ​
  640. $html = '';
  641. ​
  642. switch ($type) {
  643. case 'hero':
  644. $html = render_hero($data, $id);
  645. break;
  646. case 'blog-hero':
  647. $html = render_blog_hero($data, $id);
  648. break;
  649. case 'stats':
  650. $html = render_stats($data, $id);
  651. break;
  652. case 'features':
  653. $html = render_features($data, $id);
  654. break;
  655. case 'testimonials':
  656. $html = render_testimonials($data, $id);
  657. break;
  658. case 'pricing':
  659. $html = render_pricing($data, $id);
  660. break;
  661. case 'cta':
  662. $html = render_cta($data, $id);
  663. break;
  664. case 'text':
  665. $html = render_text($data, $id);
  666. break;
  667. case 'image':
  668. $html = render_image($data, $id);
  669. break;
  670. case 'image-text':
  671. $html = render_image_text($data, $id);
  672. break;
  673. case 'product-cards':
  674. $html = render_product_cards($data, $id);
  675. break;
  676. case 'video':
  677. $html = render_video($data, $id);
  678. break;
  679. case 'gallery':
  680. $html = render_gallery($data, $id);
  681. break;
  682. case 'faq':
  683. $html = render_faq($data, $id);
  684. break;
  685. case 'team':
  686. $html = render_team($data, $id);
  687. break;
  688. case 'audio':
  689. $html = render_audio($data, $id);
  690. break;
  691. case 'countdown':
  692. $html = render_countdown($data, $id);
  693. break;
  694. case 'newsletter':
  695. $html = render_newsletter($data, $id);
  696. break;
  697. case 'html':
  698. $html = $data['html'] ?? '';
  699. break;
  700. default:
  701. return '';
  702. }
  703. ​
  704. if (!empty($html)) {
  705. RenderCache::set($cache_key, $html);
  706. }
  707. ​
  708. if ($should_lazy_load && !empty($html)) {
  709. $block_id = LazyLoadManager::registerLazyBlock($block, $html);
  710. if ($block_id) {
  711. return LazyLoadManager::getLazyPlaceholder($block_id, $type);
  712. }
  713. }
  714. ​
  715. return $html;
  716. }
  717. ​
  718. function render_hero($d, $id = '')
  719. {
  720. if (!is_array($d)) return '';
  721. ob_start(); ?>
  722. <section class="hero" <?= $id ?>>
  723. <div class="hero-grid"></div>
  724. <div class="container hero-content">
  725. <?php if (!empty($d['badge'])): ?><div class="badge"><span class="badge-dot"></span><span><?= e($d['badge']) ?></span></div><?php endif; ?>
  726. <h1><?= e($d['title'] ?? '') ?></h1>
  727. <p><?= e($d['subtitle'] ?? '') ?></p>
  728. <div class="hero-buttons">
  729. <?php if (!empty($d['button_primary'])): ?><a href="<?= e($d['button_primary_url'] ?? '#') ?>" class="btn btn-primary"><?= e($d['button_primary']) ?></a><?php endif; ?>
  730. <?php if (!empty($d['button_secondary'])): ?><a href="<?= e($d['button_secondary_url'] ?? '#') ?>" class="btn btn-outline"><?= e($d['button_secondary']) ?></a><?php endif; ?>
  731. </div>
  732. </div>
  733. </section>
  734. <?php return ob_get_clean();
  735. }
  736. ​
  737. function render_blog_hero($d, $id = '')
  738. {
  739. if (!is_array($d)) return '';
  740. ​
  741. ob_start(); ?>
  742. <section class="blog-hero-section" <?= $id ?>>
  743. <div class="container">
  744. ​
  745. <?php if (!empty($d['image'])): ?>
  746. <div class="hero-image-wrapper" style="margin-bottom: 20px;">
  747. <img
  748. src="<?= e($d['image']) ?>"
  749. alt="<?= e($d['title'] ?? '') ?>"
  750. style="max-width: 100%; height: auto; border-radius: 8px;">
  751. </div>
  752. <?php endif; ?>
  753. ​
  754. <?php if (!empty($d['title'])): ?>
  755. <h1 style="margin-bottom: 15px;" class="blog-h1">
  756. <?= e($d['title']) ?>
  757. </h1>
  758. <?php endif; ?>
  759. ​
  760. <?php if (!empty($d['content'])): ?>
  761. <div class="text-content" style="font-size: 1.1rem; line-height: 1.6; opacity: 0.9;">
  762. <?= nl2br(e($d['content'])) ?>
  763. </div>
  764. <?php endif; ?>
  765. </div>
  766. </section>
  767. <?php
  768. return ob_get_clean();
  769. }
  770. ​
  771. function render_stats($d, $id = '')
  772. {
  773. if (!is_array($d)) return '';
  774. $items = $d['items'] ?? [];
  775. if (empty($items) || !is_array($items)) return '';
  776. ob_start(); ?>
  777. <section class="stats" <?= $id ?>>
  778. <div class="container">
  779. <div class="stats-grid">
  780. <?php foreach ($items as $item): if (!is_array($item)) continue; ?>
  781. <div class="stat-item">
  782. <h3><?= e($item['value'] ?? '') ?></h3>
  783. <p><?= e($item['label'] ?? '') ?></p>
  784. </div>
  785. <?php endforeach; ?>
  786. </div>
  787. </div>
  788. </section>
  789. <?php return ob_get_clean();
  790. }
  791. ​
  792. function render_features($d, $id = '')
  793. {
  794. if (!is_array($d)) return '';
  795. $items = $d['items'] ?? [];
  796. ob_start(); ?>
  797. <section class="features" <?= $id ?>>
  798. <div class="container">
  799. <?php if (!empty($d['title'])): ?>
  800. <div class="section-header">
  801. <h2><?= e($d['title']) ?></h2>
  802. <?php if (!empty($d['subtitle'])): ?><p><?= e($d['subtitle']) ?></p><?php endif; ?>
  803. </div>
  804. <?php endif; ?>
  805. <?php if (!empty($items) && is_array($items)): ?>
  806. <div class="features-grid">
  807. <?php foreach ($items as $item): if (!is_array($item)) continue; ?>
  808. <div class="feature-card">
  809. <div class="feature-icon"><?= render_icon($item['icon'] ?? 'bolt') ?></div>
  810. <h3><?= e($item['title'] ?? '') ?></h3>
  811. <p><?= e($item['description'] ?? '') ?></p>
  812. </div>
  813. <?php endforeach; ?>
  814. </div>
  815. <?php endif; ?>
  816. </div>
  817. </section>
  818. <?php return ob_get_clean();
  819. }
  820. ​
  821. function render_testimonials($d, $id = '')
  822. {
  823. if (!is_array($d)) return '';
  824. $items = $d['items'] ?? [];
  825. ob_start(); ?>
  826. <section class="testimonials" <?= $id ?>>
  827. <div class="container">
  828. <?php if (!empty($d['title'])): ?>
  829. <div class="section-header">
  830. <h2><?= e($d['title']) ?></h2>
  831. <?php if (!empty($d['subtitle'])): ?><p><?= e($d['subtitle']) ?></p><?php endif; ?>
  832. </div>
  833. <?php endif; ?>
  834. <?php if (!empty($items) && is_array($items)): ?>
  835. <div class="testimonials-grid">
  836. <?php foreach ($items as $item): if (!is_array($item)) continue; ?>
  837. <div class="testimonial-card">
  838. <p>"<?= e($item['quote'] ?? '') ?>"</p>
  839. <div class="testimonial-author">
  840. <div class="testimonial-avatar"><?= e($item['initials'] ?? '') ?></div>
  841. <div class="testimonial-info">
  842. <h4><?= e($item['name'] ?? '') ?></h4><span><?= e($item['role'] ?? '') ?></span>
  843. </div>
  844. </div>
  845. </div>
  846. <?php endforeach; ?>
  847. </div>
  848. <?php endif; ?>
  849. </div>
  850. </section>
  851. <?php return ob_get_clean();
  852. }
  853. ​
  854. function render_pricing($d, $id = '')
  855. {
  856. if (!is_array($d)) return '';
  857. $items = $d['items'] ?? [];
  858. ob_start(); ?>
  859. <section class="pricing" <?= $id ?>>
  860. <div class="container">
  861. <?php if (!empty($d['title'])): ?>
  862. <div class="section-header">
  863. <h2><?= e($d['title']) ?></h2>
  864. <?php if (!empty($d['subtitle'])): ?><p><?= e($d['subtitle']) ?></p><?php endif; ?>
  865. </div>
  866. <?php endif; ?>
  867. <?php if (!empty($items) && is_array($items)): ?>
  868. <div class="pricing-grid">
  869. <?php foreach ($items as $item): if (!is_array($item)) continue; ?>
  870. <div class="pricing-card <?= !empty($item['featured']) ? 'featured' : '' ?>">
  871. <?php if (!empty($item['featured'])): ?><span class="pricing-popular">Most Popular</span><?php endif; ?>
  872. <h3><?= e($item['name'] ?? '') ?></h3>
  873. <div class="price"><?= e($item['price'] ?? '') ?><span><?= e($item['period'] ?? '') ?></span></div>
  874. <p class="description"><?= e($item['description'] ?? '') ?></p>
  875. <?php if (!empty($item['features']) && is_array($item['features'])): ?>
  876. <ul class="pricing-features">
  877. <?php foreach ($item['features'] as $feature): ?>
  878. <li><svg width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
  879. <polyline points="20 6 9 17 4 12" />
  880. </svg><?= e($feature) ?></li>
  881. <?php endforeach; ?>
  882. </ul>
  883. <?php endif; ?>
  884. <a href="<?= e($item['button_url'] ?? '#') ?>" class="btn <?= !empty($item['featured']) ? 'btn-primary' : 'btn-outline' ?>"><?= e($item['button_text'] ?? $item['button'] ?? 'Get Started') ?></a>
  885. </div>
  886. <?php endforeach; ?>
  887. </div>
  888. <?php endif; ?>
  889. </div>
  890. </section>
  891. <?php return ob_get_clean();
  892. }
  893. ​
  894. function render_cta($d, $id = '')
  895. {
  896. if (!is_array($d)) return '';
  897. ob_start(); ?>
  898. <section class="cta" <?= $id ?>>
  899. <div class="container">
  900. <div class="cta-inner">
  901. <h2><?= e($d['title'] ?? '') ?></h2>
  902. <p><?= e($d['subtitle'] ?? '') ?></p>
  903. <div class="cta-buttons">
  904. <?php if (!empty($d['button_primary'])): ?><a href="<?= e($d['button_primary_url'] ?? '#') ?>" class="btn btn-primary"><?= e($d['button_primary']) ?></a><?php endif; ?>
  905. <?php if (!empty($d['button_secondary'])): ?><a href="<?= e($d['button_secondary_url'] ?? '#') ?>" class="btn btn-outline"><?= e($d['button_secondary']) ?></a><?php endif; ?>
  906. </div>
  907. </div>
  908. </div>
  909. </section>
  910. <?php return ob_get_clean();
  911. }
  912. ​
  913. function render_text($d, $id = '')
  914. {
  915. if (!is_array($d)) return '';
  916. ob_start(); ?>
  917. <section class="text-section" <?= $id ?>>
  918. <div class="container">
  919. <?php if (!empty($d['title'])): ?><h2><?= e($d['title']) ?></h2><?php endif; ?>
  920. <div class="text-content"><?= nl2br(e($d['content'] ?? '')) ?></div>
  921. </div>
  922. </section>
  923. <?php return ob_get_clean();
  924. }
  925. ​
  926. function render_image($d, $id = '')
  927. {
  928. if (!is_array($d) || empty($d['url'])) return '';
  929. ob_start(); ?>
  930. <section class="image-section" <?= $id ?>>
  931. <div class="container">
  932. <figure>
  933. <img src="<?= e($d['url']) ?>" alt="<?= e($d['alt'] ?? '') ?>" loading="lazy">
  934. <?php if (!empty($d['caption'])): ?><figcaption><?= e($d['caption']) ?></figcaption><?php endif; ?>
  935. </figure>
  936. </div>
  937. </section>
  938. <?php return ob_get_clean();
  939. }
  940. ​
  941. function render_image_text($d, $id = '')
  942. {
  943. if (!is_array($d)) return '';
  944. $allowed_positions = ['left', 'right'];
  945. $image_pos = in_array($d['image_position'] ?? '', $allowed_positions, true)
  946. ? $d['image_position']
  947. : 'left';
  948. ​
  949. ob_start(); ?>
  950. <section class="image-text" <?= $id ?>>
  951. <div class="container">
  952. <div class="image-text-grid image-text-<?= e($image_pos) ?>">
  953. <div class="image-text-image">
  954. <img src="<?= e($d['image_url'] ?? '') ?>" alt="<?= e($d['image_alt'] ?? '') ?>" loading="lazy">
  955. </div>
  956. <div class="image-text-content">
  957. <?php if (!empty($d['subtitle'])): ?><p class="section-subtitle"><?= e($d['subtitle']) ?></p><?php endif; ?>
  958. <?php if (!empty($d['title'])): ?><h2><?= e($d['title']) ?></h2><?php endif; ?>
  959. <div class="text-content"><?= nl2br(e($d['content'] ?? '')) ?></div>
  960. <?php if (!empty($d['button_text'])): ?>
  961. <a href="<?= e($d['button_url'] ?? '#') ?>" class="btn btn-primary"><?= e($d['button_text']) ?></a>
  962. <?php endif; ?>
  963. </div>
  964. </div>
  965. </div>
  966. </section>
  967. <?php return ob_get_clean();
  968. }
  969. ​
  970. function render_product_cards($d, $id = '')
  971. {
  972. if (!is_array($d)) return '';
  973. $products = $d['products'] ?? [];
  974. if (empty($products) || !is_array($products)) return '';
  975. ob_start(); ?>
  976. <section class="product-cards" <?= $id ?>>
  977. <div class="container">
  978. <?php if (!empty($d['title'])): ?>
  979. <div class="section-header">
  980. <h2><?= e($d['title']) ?></h2>
  981. <?php if (!empty($d['subtitle'])): ?><p><?= e($d['subtitle']) ?></p><?php endif; ?>
  982. </div>
  983. <?php endif; ?>
  984. <div class="products-grid">
  985. <?php foreach ($products as $product): if (!is_array($product)) continue; ?>
  986. <div class="product-card">
  987. <?php if (!empty($product['image'])): ?>
  988. <div class="product-image">
  989. <img src="<?= e($product['image']) ?>" alt="<?= e($product['title'] ?? '') ?>" width="48" height="48" loading="lazy">
  990. </div>
  991. <?php endif; ?>
  992. <h3><?= e($product['title'] ?? '') ?></h3>
  993. <p class="product-description"><?= e($product['description'] ?? '') ?></p>
  994. <?php if (!empty($product['features']) && is_array($product['features'])): ?>
  995. <ul class="product-features">
  996. <?php foreach ($product['features'] as $feature): ?>
  997. <li><?= e($feature) ?></li>
  998. <?php endforeach; ?>
  999. </ul>
  1000. <?php endif; ?>
  1001. <?php if (!empty($product['button_text'])): ?>
  1002. <a href="<?= e($product['button_url'] ?? '#') ?>" class="btn btn-primary btn-sm"><?= e($product['button_text']) ?></a>
  1003. <?php endif; ?>
  1004. </div>
  1005. <?php endforeach; ?>
  1006. </div>
  1007. </div>
  1008. </section>
  1009. <?php return ob_get_clean();
  1010. }
  1011. ​
  1012. function render_video($d, $id = '')
  1013. {
  1014. if (!is_array($d) || empty($d['url'])) return '';
  1015. ​
  1016. $url = $d['url'];
  1017. $type = $d['type'] ?? 'url';
  1018. $poster = !empty($d['posterUrl']) ? $d['posterUrl'] : '';
  1019. $playIcon = !empty($d['playIcon']) ? $d['playIcon'] : '/assets/images/play.webp';
  1020. $pauseIcon = !empty($d['pauseIcon']) ? $d['pauseIcon'] : '/assets/images/pause.webp';
  1021. $reloadIcon = !empty($d['reloadIcon']) ? $d['reloadIcon'] : '/assets/images/reload.webp';
  1022. ​
  1023. $id_attr = '';
  1024. if (!empty($id)) {
  1025. $safe_id = preg_replace('/[^a-zA-Z0-9_-]/', '', $id);
  1026. $id_attr = ' id="' . e($safe_id) . '"';
  1027. }
  1028. ​
  1029. $allowed_types = ['url', 'youtube', 'facebook'];
  1030. if (!in_array($type, $allowed_types, true)) {
  1031. $type = 'url';
  1032. }
  1033. ​
  1034. if ($type === 'url') {
  1035. if (preg_match('/youtube\.com|youtu\.be/i', $url)) {
  1036. $type = 'youtube';
  1037. } elseif (preg_match('/facebook\.com|fb\.watch/i', $url)) {
  1038. $type = 'facebook';
  1039. }
  1040. }
  1041. ​
  1042. $video_unique_id = 'video-' . uniqid();
  1043. ​
  1044. ob_start(); ?>
  1045. <section class="video-section" <?= $id_attr ?>>
  1046. <div class="container">
  1047. <?php if (!empty($d['title'])): ?>
  1048. <div class="section-header">
  1049. <h2><?= e($d['title']) ?></h2>
  1050. <?php if (!empty($d['subtitle'])): ?><p><?= e($d['subtitle']) ?></p><?php endif; ?>
  1051. </div>
  1052. <?php endif; ?>
  1053. <div class="video-wrapper" style="position: relative;">
  1054. <?php
  1055. if ($poster && ($type === 'youtube' || $type === 'facebook')): ?>
  1056. <div class="video-overlay" style="background-image: url('<?= e($poster) ?>'); position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-size: cover; background-position: center; z-index: 10; cursor: pointer; display: flex; align-items: center; justify-content: center;" onclick="this.style.display='none';" role="button" aria-label="Odtwórz wideo" tabindex="0" onkeypress="if(event.key==='Enter'||event.key===' '){this.style.display='none';}">
  1057. <picture>
  1058. <source srcset="<?= e($playIcon) ?>" type="image/webp">
  1059. <img src="<?= e(str_replace('.webp', '.png', $playIcon)) ?>"
  1060. alt="Play"
  1061. style="width: 80px;
  1062. height: 80px;
  1063. transition: transform 0.2s;
  1064. filter: drop-shadow(0 4px 8px rgba(0,0,0,0.4));"
  1065. onmouseover="this.style.transform='scale(1.1)'"
  1066. onmouseout="this.style.transform='scale(1)'"
  1067. loading="lazy">
  1068. </picture>
  1069. </div>
  1070. <?php endif; ?>
  1071. ​
  1072. <?php if ($type === 'youtube'):
  1073. preg_match('/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/i', $url, $matches);
  1074. $video_id = $matches[1] ?? '';
  1075. if ($video_id && preg_match('/^[a-zA-Z0-9_-]{11}$/', $video_id)):
  1076. $yt_src = "https://www.youtube.com/embed/" . e($video_id);
  1077. $yt_src .= $poster ? "?autoplay=1&rel=0" : "?rel=0";
  1078. ?>
  1079. <iframe src="<?= $yt_src ?>"
  1080. frameborder="0"
  1081. allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
  1082. allowfullscreen
  1083. loading="lazy"
  1084. title="<?= !empty($d['title']) ? e($d['title']) : 'YouTube video' ?>"></iframe>
  1085. <?php endif;
  1086. elseif ($type === 'facebook'):
  1087. $fb_src = "https://www.facebook.com/plugins/video.php?href=" . urlencode($url) . "&show_text=false";
  1088. $fb_src .= $poster ? "&autoplay=true" : "";
  1089. ?>
  1090. <iframe src="<?= e($fb_src) ?>"
  1091. frameborder="0"
  1092. allow="autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share"
  1093. allowfullscreen
  1094. loading="lazy"
  1095. title="<?= !empty($d['title']) ? e($d['title']) : 'Facebook video' ?>"></iframe>
  1096. <?php else: ?>
  1097. <video id="<?= $video_unique_id ?>"
  1098. preload="metadata"
  1099. <?= $poster ? 'poster="' . e($poster) . '"' : '' ?>
  1100. controlsList="nodownload">
  1101. <source src="<?= e($url) ?>" type="<?= e(media_mime_for_url($url, 'video/mp4')) ?>">
  1102. Your browser does not support the video tag.
  1103. </video>
  1104. ​
  1105. <button class="video-play-pause-btn" id="play-pause-<?= $video_unique_id ?>" style=" position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 15%; min-width: 90px; max-width: 150px; aspect-ratio: 1/1; background: none; border: none; cursor: pointer; z-index: 5; opacity: 1; transition: opacity 0.3s; pointer-events: auto; " aria-label="Play/Pause">
  1106. <picture>
  1107. <source srcset="<?= e($playIcon) ?>" type="image/webp">
  1108. <img class="play-icon" src="<?= e(str_replace('.webp', '.png', $playIcon)) ?>" alt="Play" style="width: 100%; height: 100%; filter: drop-shadow(0 4px 8px rgba(0,0,0,0.4));">
  1109. </picture>
  1110. <picture style="display: none;">
  1111. <source srcset="<?= e($pauseIcon) ?>" type="image/webp">
  1112. <img class="pause-icon" src="<?= e(str_replace('.webp', '.png', $pauseIcon)) ?>" alt="Pause" style="width: 100%; height: 100%; filter: drop-shadow(0 4px 8px rgba(0,0,0,0.4));">
  1113. </picture>
  1114. ​
  1115. <picture class="reload-pic" style="display: none;">
  1116. <?php $reloadIcon = str_replace(basename($playIcon), 'reload.webp', $playIcon); ?>
  1117. <source srcset="<?= e($reloadIcon) ?>" type="image/webp">
  1118. <img class="reload-icon" src="<?= e(str_replace('.webp', '.png', $reloadIcon)) ?>" alt="Pause" style="width: 100%; height: 100%; filter: drop-shadow(0 4px 8px rgba(0,0,0,0.4));">
  1119. </picture>
  1120. </button>
  1121. <script>
  1122. (function() {
  1123. const video = document.getElementById('<?= $video_unique_id ?>');
  1124. const btn = document.getElementById('play-pause-<?= $video_unique_id ?>');
  1125. const playPicture = btn.querySelector('picture:nth-child(1)');
  1126. const pausePicture = btn.querySelector('picture:nth-child(2)');
  1127. const reloadPicture = btn.querySelector('picture.reload-pic'); // Pobranie nowej ikony
  1128. const wrapper = btn.parentElement;
  1129. let hideTimeout;
  1130. ​
  1131. function showButton() {
  1132. btn.style.opacity = '1';
  1133. clearTimeout(hideTimeout);
  1134. hideTimeout = setTimeout(() => {
  1135. if (!video.paused && !video.ended) {
  1136. btn.style.opacity = '0';
  1137. }
  1138. }, 2000);
  1139. }
  1140. ​
  1141. function updateButton() {
  1142. playPicture.style.display = 'none';
  1143. pausePicture.style.display = 'none';
  1144. reloadPicture.style.display = 'none';
  1145. ​
  1146. if (video.ended) {
  1147. reloadPicture.style.display = '';
  1148. btn.style.opacity = '1';
  1149. } else if (video.paused) {
  1150. playPicture.style.display = '';
  1151. btn.style.opacity = '1';
  1152. } else {
  1153. pausePicture.style.display = '';
  1154. showButton();
  1155. }
  1156. }
  1157. ​
  1158. btn.addEventListener('click', function(e) {
  1159. e.preventDefault();
  1160. e.stopPropagation();
  1161. ​
  1162. if (video.ended) {
  1163. video.currentTime = 0;
  1164. video.play();
  1165. } else if (video.paused) {
  1166. video.play();
  1167. } else {
  1168. video.pause();
  1169. }
  1170. });
  1171. ​
  1172. video.addEventListener('play', updateButton);
  1173. video.addEventListener('pause', updateButton);
  1174. video.addEventListener('ended', updateButton);
  1175. ​
  1176. wrapper.addEventListener('mousemove', showButton);
  1177. wrapper.addEventListener('mouseenter', showButton);
  1178. ​
  1179. updateButton();
  1180. })();
  1181. </script>
  1182. <?php endif; ?>
  1183. </div>
  1184. <?php if (!empty($d['caption'])): ?>
  1185. <p class="video-caption"><?= e($d['caption']) ?></p>
  1186. <?php endif; ?>
  1187. </div>
  1188. </section>
  1189. <?php return ob_get_clean();
  1190. }
  1191. ​
  1192. function render_gallery($d, $id = '')
  1193. {
  1194. if (!is_array($d)) return '';
  1195. $images = $d['images'] ?? [];
  1196. if (empty($images) || !is_array($images)) return '';
  1197. ob_start(); ?>
  1198. <section class="gallery-section" <?= $id ?>>
  1199. <div class="container">
  1200. <?php if (!empty($d['title'])): ?>
  1201. <div class="section-header">
  1202. <h2><?= e($d['title']) ?></h2>
  1203. <?php if (!empty($d['subtitle'])): ?><p><?= e($d['subtitle']) ?></p><?php endif; ?>
  1204. </div>
  1205. <?php endif; ?>
  1206. <div class="gallery-grid">
  1207. <?php foreach ($images as $image): if (!is_array($image)) continue; ?>
  1208. <div class="gallery-item">
  1209. <img src="<?= e($image['url'] ?? '') ?>" alt="<?= e($image['alt'] ?? '') ?>" loading="lazy">
  1210. <?php if (!empty($image['caption'])): ?>
  1211. <div class="gallery-caption"><?= e($image['caption']) ?></div>
  1212. <?php endif; ?>
  1213. </div>
  1214. <?php endforeach; ?>
  1215. </div>
  1216. </div>
  1217. </section>
  1218. <?php return ob_get_clean();
  1219. }
  1220. ​
  1221. function render_faq($d, $id = '')
  1222. {
  1223. if (!is_array($d)) return '';
  1224. $items = $d['items'] ?? [];
  1225. if (empty($items) || !is_array($items)) return '';
  1226. ob_start(); ?>
  1227. <section class="faq-section" <?= $id ?>>
  1228. <div class="container">
  1229. <?php if (!empty($d['title'])): ?>
  1230. <div class="section-header">
  1231. <h2><?= e($d['title']) ?></h2>
  1232. <?php if (!empty($d['subtitle'])): ?><p><?= e($d['subtitle']) ?></p><?php endif; ?>
  1233. </div>
  1234. <?php endif; ?>
  1235. <div class="faq-list">
  1236. <?php foreach ($items as $item): if (!is_array($item)) continue; ?>
  1237. <details class="faq-item">
  1238. <summary class="faq-question"><?= e($item['question'] ?? '') ?></summary>
  1239. <div class="faq-answer"><?= nl2br(e($item['answer'] ?? '')) ?></div>
  1240. </details>
  1241. <?php endforeach; ?>
  1242. </div>
  1243. </div>
  1244. </section>
  1245. <?php return ob_get_clean();
  1246. }
  1247. ​
  1248. function render_team($d, $id = '')
  1249. {
  1250. if (!is_array($d)) return '';
  1251. $members = $d['members'] ?? [];
  1252. if (empty($members) || !is_array($members)) return '';
  1253. ob_start(); ?>
  1254. <section class="team-section" <?= $id ?>>
  1255. <div class="container">
  1256. <?php if (!empty($d['title'])): ?>
  1257. <div class="section-header">
  1258. <h2><?= e($d['title']) ?></h2>
  1259. <?php if (!empty($d['subtitle'])): ?><p><?= e($d['subtitle']) ?></p><?php endif; ?>
  1260. </div>
  1261. <?php endif; ?>
  1262. <div class="team-grid">
  1263. <?php foreach ($members as $member): if (!is_array($member)) continue; ?>
  1264. <div class="team-member">
  1265. <?php if (!empty($member['image'])): ?>
  1266. <img src="<?= e($member['image']) ?>" alt="<?= e($member['name'] ?? '') ?>" class="team-photo" loading="lazy">
  1267. <?php else: ?>
  1268. <div class="team-avatar"><?= e($member['initials'] ?? substr($member['name'] ?? '?', 0, 2)) ?></div>
  1269. <?php endif; ?>
  1270. <h3><?= e($member['name'] ?? '') ?></h3>
  1271. <p class="team-role"><?= e($member['role'] ?? '') ?></p>
  1272. <?php if (!empty($member['bio'])): ?>
  1273. <p class="team-bio"><?= e($member['bio']) ?></p>
  1274. <?php endif; ?>
  1275. <?php if (!empty($member['social']) && is_array($member['social'])): ?>
  1276. <div class="team-social">
  1277. <?php foreach ($member['social'] as $social): if (!is_array($social)) continue; ?>
  1278. <a href="<?= e($social['url'] ?? '#') ?>" target="_blank" rel="noopener" class="team-social-link">
  1279. <?= render_social_icon($social['platform'] ?? 'twitter') ?>
  1280. </a>
  1281. <?php endforeach; ?>
  1282. </div>
  1283. <?php endif; ?>
  1284. </div>
  1285. <?php endforeach; ?>
  1286. </div>
  1287. </div>
  1288. </section>
  1289. <?php return ob_get_clean();
  1290. }
  1291. ​
  1292. function render_audio($d, $id = '')
  1293. {
  1294. if (!is_array($d) || empty($d['url'])) return '';
  1295. ob_start(); ?>
  1296. <section class="audio-section" <?= $id ?>>
  1297. <div class="container">
  1298. <?php if (!empty($d['title'])): ?>
  1299. <div class="section-header">
  1300. <h2><?= e($d['title']) ?></h2>
  1301. <?php if (!empty($d['subtitle'])): ?><p><?= e($d['subtitle']) ?></p><?php endif; ?>
  1302. </div>
  1303. <?php endif; ?>
  1304. <div class="audio-wrapper">
  1305. <audio controls preload="metadata" class="audio-player">
  1306. <source src="<?= e($d['url']) ?>" type="<?= e(media_mime_for_url($d['url'], 'audio/mpeg')) ?>">
  1307. Your browser does not support the audio element.
  1308. </audio>
  1309. <?php if (!empty($d['music_link'])): ?>
  1310. <a href="<?= e($d['music_link']) ?>" class="btn btn-outline" target="_blank" rel="noopener"><?= e($d['music_link_text'] ?? 'Download') ?></a>
  1311. <?php endif; ?>
  1312. </div>
  1313. </div>
  1314. </section>
  1315. <?php return ob_get_clean();
  1316. }
  1317. ​
  1318. function render_countdown($d, $id = '')
  1319. {
  1320. if (!is_array($d) || empty($d['target_date'])) return '';
  1321. ​
  1322. $target_date = trim((string) $d['target_date']);
  1323. $target_time = trim((string) ($d['target_time'] ?? '00:00'));
  1324. ​
  1325. if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $target_date)) {
  1326. return '';
  1327. }
  1328. if (!preg_match('/^([01]\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/', $target_time)) {
  1329. $target_time = '00:00';
  1330. }
  1331. ​
  1332. $target_datetime = $target_date . ' ' . $target_time;
  1333. $timer_id = 'timer_' . hash('sha256', $target_datetime . random_bytes(8));
  1334. ​
  1335. ob_start(); ?>
  1336. <section class="countdown-section" <?= $id ?>>
  1337. <div class="container">
  1338. <?php if (!empty($d['title'])): ?>
  1339. <div class="section-header">
  1340. <h2><?= e($d['title']) ?></h2>
  1341. <?php if (!empty($d['subtitle'])): ?><p><?= e($d['subtitle']) ?></p><?php endif; ?>
  1342. </div>
  1343. <?php endif; ?>
  1344. <div class="countdown-timer" id="<?= e($timer_id) ?>" data-target="<?= e($target_datetime) ?>">
  1345. <div class="countdown-item">
  1346. <div class="countdown-value" data-days>00</div>
  1347. <div class="countdown-label">Days</div>
  1348. </div>
  1349. <div class="countdown-item">
  1350. <div class="countdown-value" data-hours>00</div>
  1351. <div class="countdown-label">Hours</div>
  1352. </div>
  1353. <div class="countdown-item">
  1354. <div class="countdown-value" data-minutes>00</div>
  1355. <div class="countdown-label">Minutes</div>
  1356. </div>
  1357. <div class="countdown-item">
  1358. <div class="countdown-value" data-seconds>00</div>
  1359. <div class="countdown-label">Seconds</div>
  1360. </div>
  1361. </div>
  1362. </div>
  1363. </section>
  1364. <script>
  1365. (function() {
  1366. 'use strict';
  1367. const timer = document.getElementById(<?= json_encode($timer_id) ?>);
  1368. if (!timer) return;
  1369. ​
  1370. const targetDateStr = <?= json_encode($target_datetime) ?>;
  1371. const targetDate = new Date(targetDateStr.replace(' ', 'T') + 'Z').getTime();
  1372. if (isNaN(targetDate)) return;
  1373. ​
  1374. function updateCountdown() {
  1375. const now = Date.now();
  1376. const distance = targetDate - now;
  1377. ​
  1378. if (distance < 0) {
  1379. timer.querySelector('[data-days]').textContent = '00';
  1380. timer.querySelector('[data-hours]').textContent = '00';
  1381. timer.querySelector('[data-minutes]').textContent = '00';
  1382. timer.querySelector('[data-seconds]').textContent = '00';
  1383. return;
  1384. }
  1385. ​
  1386. const days = Math.floor(distance / (1000 * 60 * 60 * 24));
  1387. const hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
  1388. const minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
  1389. const seconds = Math.floor((distance % (1000 * 60)) / 1000);
  1390. ​
  1391. timer.querySelector('[data-days]').textContent = String(days).padStart(2, '0');
  1392. timer.querySelector('[data-hours]').textContent = String(hours).padStart(2, '0');
  1393. timer.querySelector('[data-minutes]').textContent = String(minutes).padStart(2, '0');
  1394. timer.querySelector('[data-seconds]').textContent = String(seconds).padStart(2, '0');
  1395. }
  1396. ​
  1397. updateCountdown();
  1398. setInterval(updateCountdown, 1000);
  1399. })();
  1400. </script>
  1401. <?php return ob_get_clean();
  1402. }
  1403. ​
  1404. function render_newsletter($d, $id = '')
  1405. {
  1406. if (!is_array($d)) return '';
  1407. ​
  1408. $section_id = '';
  1409. if (!empty($id) && preg_match('/id="([^"]+)"/', $id, $m)) {
  1410. $section_id = $m[1];
  1411. }
  1412. ​
  1413. ob_start(); ?>
  1414. <section class="newsletter-section" <?= $id ?>>
  1415. <?php if ($section_id !== 'newsletter'): ?>
  1416. <div id="newsletter" style="height:1px; margin-top:-1px;"></div>
  1417. <?php endif; ?>
  1418. <div class="container">
  1419. <div class="newsletter-inner">
  1420. <?php if (!empty($d['title'])): ?>
  1421. <h2><?= e($d['title']) ?></h2>
  1422. <?php endif; ?>
  1423. <?php if (!empty($d['subtitle'])): ?>
  1424. <p><?= e($d['subtitle']) ?></p>
  1425. <?php endif; ?>
  1426. <form class="newsletter-form" method="POST" action="/newsletter/subscribe">
  1427. <input
  1428. type="email"
  1429. name="email"
  1430. class="newsletter-input"
  1431. placeholder="<?= e($d['placeholder'] ?? 'Enter your email') ?>"
  1432. required>
  1433. <button type="submit" class="btn btn-primary">
  1434. <?= e($d['button_text'] ?? 'Subscribe') ?>
  1435. </button>
  1436. </form>
  1437. </div>
  1438. </div>
  1439. </section>
  1440. <?php
  1441. return ob_get_clean();
  1442. }
  1443. ​