AddonCard.js
3 weeks ago
CardsGrid.js
3 months ago
Categories.js
4 months ago
Filters.js
3 months ago
modules-api.js
1 year ago
AddonCard.js
569 lines
| 1 | /** |
| 2 | * External Dependencies |
| 3 | */ |
| 4 | import { |
| 5 | Badge, |
| 6 | Box, |
| 7 | Button, |
| 8 | Flex, |
| 9 | Heading, |
| 10 | HStack, |
| 11 | Icon, |
| 12 | IconButton, |
| 13 | Link, |
| 14 | Modal, |
| 15 | ModalBody, |
| 16 | ModalCloseButton, |
| 17 | ModalContent, |
| 18 | ModalHeader, |
| 19 | ModalOverlay, |
| 20 | Spinner, |
| 21 | Switch, |
| 22 | Text, |
| 23 | Tooltip, |
| 24 | useDisclosure, |
| 25 | VStack, |
| 26 | } from '@chakra-ui/react'; |
| 27 | import { __ } from '@wordpress/i18n'; |
| 28 | import { useEffect, useMemo, useState } from 'react'; |
| 29 | import { FaCog, FaPlay } from 'react-icons/fa'; |
| 30 | import ReactPlayer from 'react-player'; |
| 31 | import { activateModule, deactivateModule } from './modules-api'; |
| 32 | |
| 33 | const AddonCard = ({ addon, showToast, onModuleToggle }) => { |
| 34 | const [isActive, setIsActive] = useState(addon.status === 'active'); |
| 35 | const [isLoading, setIsLoading] = useState(false); |
| 36 | const [videoLoading, setVideoLoading] = useState(false); |
| 37 | const { |
| 38 | isOpen: isVideoOpen, |
| 39 | onOpen: onVideoOpen, |
| 40 | onClose: onVideoClose, |
| 41 | } = useDisclosure(); |
| 42 | |
| 43 | const { isPro, licensePlan } = |
| 44 | typeof _EVF_DASHBOARD_ !== 'undefined' && _EVF_DASHBOARD_ |
| 45 | ? _EVF_DASHBOARD_ |
| 46 | : {}; |
| 47 | |
| 48 | const getImageUrl = (imagePath) => { |
| 49 | const { assetsURL } = |
| 50 | typeof _EVF_DASHBOARD_ !== 'undefined' && _EVF_DASHBOARD_; |
| 51 | if (imagePath && assetsURL) { |
| 52 | return assetsURL + imagePath; |
| 53 | } |
| 54 | return imagePath; |
| 55 | }; |
| 56 | |
| 57 | const moduleEnabled = useMemo(() => { |
| 58 | if (!addon.plan || addon.plan.length === 0) { |
| 59 | return true; |
| 60 | } |
| 61 | |
| 62 | if (Array.isArray(addon.plan) && addon.plan.includes('free')) { |
| 63 | return true; |
| 64 | } |
| 65 | |
| 66 | if (isPro) { |
| 67 | return true; |
| 68 | } |
| 69 | |
| 70 | return false; |
| 71 | }, [addon.plan, isPro]); |
| 72 | |
| 73 | const handleUpgradePlan = () => { |
| 74 | const { upgradeURL } = |
| 75 | typeof _EVF_DASHBOARD_ !== 'undefined' && _EVF_DASHBOARD_; |
| 76 | if (upgradeURL) { |
| 77 | const plan_upgrade_url = |
| 78 | upgradeURL + |
| 79 | '?utm_source=dashboard-modules&utm_medium=upgrade-button&utm_campaign=' + |
| 80 | addon.slug; |
| 81 | window.open(plan_upgrade_url, '_blank'); |
| 82 | } |
| 83 | }; |
| 84 | |
| 85 | const handleVideoPlay = () => { |
| 86 | setVideoLoading(true); |
| 87 | onVideoOpen(); |
| 88 | }; |
| 89 | |
| 90 | // Sync local isActive state when addon.status changes (e.g. after category switch) |
| 91 | useEffect(() => { |
| 92 | setIsActive(addon.status === 'active'); |
| 93 | }, [addon.status]); |
| 94 | |
| 95 | const MENU_MAP = { |
| 96 | 'everest-forms-coupons' : { |
| 97 | title: __('Coupons', 'everest-forms'), |
| 98 | page: "evf-coupons", |
| 99 | }, |
| 100 | 'everest-forms-email-templates': { |
| 101 | title: __("Email Templates", "everest-forms"), |
| 102 | page: "evf-email-templates", |
| 103 | }, |
| 104 | 'everest-forms-frontend-listing':{ |
| 105 | title: __("Frontend List", "everest-forms"), |
| 106 | page: "evf-frontend-list", |
| 107 | } |
| 108 | }; |
| 109 | |
| 110 | const appendEverestFormsSubmenu = function (title, pageSlug) { |
| 111 | const submenu = document.querySelector( |
| 112 | "#toplevel_page_everest-forms .wp-submenu" |
| 113 | ); |
| 114 | |
| 115 | if (!submenu) return; |
| 116 | |
| 117 | var exists = submenu.querySelector( |
| 118 | 'a[href="admin.php?page=' + pageSlug + '"]' |
| 119 | ); |
| 120 | if (exists) return; |
| 121 | |
| 122 | var li = document.createElement("li"); |
| 123 | var a = document.createElement("a"); |
| 124 | |
| 125 | a.href = "admin.php?page=" + pageSlug; |
| 126 | a.textContent = title; |
| 127 | |
| 128 | li.appendChild(a); |
| 129 | |
| 130 | var settings = submenu.querySelector( |
| 131 | 'a[href="admin.php?page=evf-settings"]' |
| 132 | ); |
| 133 | |
| 134 | if (settings && settings.parentElement) { |
| 135 | submenu.insertBefore(li, settings.parentElement); |
| 136 | } else { |
| 137 | submenu.appendChild(li); |
| 138 | } |
| 139 | }; |
| 140 | |
| 141 | const removeEverestFormsSubmenu = function (pageSlug) { |
| 142 | const submenu = document.querySelector( |
| 143 | "#toplevel_page_everest-forms .wp-submenu" |
| 144 | ); |
| 145 | |
| 146 | if (!submenu) return; |
| 147 | |
| 148 | var link = submenu.querySelector( |
| 149 | 'a[href="admin.php?page=' + pageSlug + '"]' |
| 150 | ); |
| 151 | |
| 152 | if (link && link.parentElement) { |
| 153 | link.parentElement.remove(); |
| 154 | } |
| 155 | }; |
| 156 | |
| 157 | const syncSidebarMenu = function (slug, enabled) { |
| 158 | var config = MENU_MAP[slug]; |
| 159 | if (!config) return; |
| 160 | |
| 161 | if (enabled) { |
| 162 | appendEverestFormsSubmenu(config.title, config.page); |
| 163 | } else { |
| 164 | removeEverestFormsSubmenu(config.page); |
| 165 | } |
| 166 | }; |
| 167 | |
| 168 | const handleToggle = async () => { |
| 169 | setIsLoading(true); |
| 170 | try { |
| 171 | let response; |
| 172 | |
| 173 | if (isActive) { |
| 174 | response = await deactivateModule(addon.slug, addon.type); |
| 175 | |
| 176 | if (response.success) { |
| 177 | setIsActive(false); |
| 178 | onModuleToggle?.(addon.slug, 'inactive'); |
| 179 | |
| 180 | syncSidebarMenu(response.slug || addon.slug, false); |
| 181 | |
| 182 | showToast( |
| 183 | response.message || 'Module deactivated successfully', |
| 184 | 'success', |
| 185 | ); |
| 186 | } else { |
| 187 | showToast( |
| 188 | response.message || 'Failed to deactivate module', |
| 189 | 'error', |
| 190 | ); |
| 191 | } |
| 192 | } else { |
| 193 | response = await activateModule(addon.slug, addon.name, addon.type); |
| 194 | if (response.success) { |
| 195 | setIsActive(true); |
| 196 | onModuleToggle?.(addon.slug, 'active'); |
| 197 | |
| 198 | syncSidebarMenu(response.slug || addon.slug, true); |
| 199 | |
| 200 | showToast( |
| 201 | response.message || 'Module activated successfully', |
| 202 | 'success', |
| 203 | ); |
| 204 | } else { |
| 205 | showToast(response.message || 'Failed to activate module', 'error'); |
| 206 | } |
| 207 | } |
| 208 | } catch (error) { |
| 209 | showToast(error.message || 'An error occurred', 'error'); |
| 210 | } |
| 211 | setIsLoading(false); |
| 212 | }; |
| 213 | |
| 214 | const getPlanBadge = (plan) => { |
| 215 | if (!plan || plan.length === 0) { |
| 216 | return 'Free'; |
| 217 | } |
| 218 | |
| 219 | if (Array.isArray(plan) && plan.includes('free')) { |
| 220 | return 'Free'; |
| 221 | } |
| 222 | return 'PRO'; |
| 223 | }; |
| 224 | |
| 225 | const getPlanBadgeStyles = (plan) => { |
| 226 | const badge = getPlanBadge(plan); |
| 227 | |
| 228 | if (badge === 'Free') { |
| 229 | return { |
| 230 | bg: 'transparent', |
| 231 | border: '1px solid #D1D5DB', |
| 232 | color: '#6B7280', |
| 233 | fontSize: '11px', |
| 234 | fontWeight: '500', |
| 235 | }; |
| 236 | } |
| 237 | |
| 238 | return { |
| 239 | bg: '#FFFAF5', |
| 240 | border: '1px solid #FF8C39', |
| 241 | color: '#FF8C39', |
| 242 | fontSize: '11px', |
| 243 | fontWeight: '600', |
| 244 | }; |
| 245 | }; |
| 246 | |
| 247 | const badgeStyles = getPlanBadgeStyles(addon.plan); |
| 248 | |
| 249 | return ( |
| 250 | <Box |
| 251 | bg="white" |
| 252 | borderRadius="lg" |
| 253 | border="1px solid" |
| 254 | borderColor="gray.200" |
| 255 | p={{ base: '4', sm: '5', md: '6' }} |
| 256 | _hover={{ boxShadow: 'md' }} |
| 257 | transition="all 0.2s" |
| 258 | position="relative" |
| 259 | height="100%" |
| 260 | display="flex" |
| 261 | flexDirection="column" |
| 262 | > |
| 263 | {/* Loading Overlay */} |
| 264 | {isLoading && ( |
| 265 | <Flex |
| 266 | position="absolute" |
| 267 | top="0" |
| 268 | left="0" |
| 269 | right="0" |
| 270 | bottom="0" |
| 271 | bg="rgba(255, 255, 255, 0.8)" |
| 272 | borderRadius="xl" |
| 273 | alignItems="center" |
| 274 | justifyContent="center" |
| 275 | zIndex="10" |
| 276 | > |
| 277 | <Spinner size="lg" color="#7545bb" thickness="3px" /> |
| 278 | </Flex> |
| 279 | )} |
| 280 | |
| 281 | <HStack |
| 282 | align="start" |
| 283 | spacing={{ base: '3', sm: '4' }} |
| 284 | flex="1" |
| 285 | mb={{ base: '4', sm: '5', md: '6' }} |
| 286 | > |
| 287 | <Box |
| 288 | w={{ base: '9', sm: '10' }} |
| 289 | h={{ base: '9', sm: '10' }} |
| 290 | bg="gray.50" |
| 291 | borderRadius="full" |
| 292 | display="flex" |
| 293 | alignItems="center" |
| 294 | justifyContent="center" |
| 295 | flexShrink={0} |
| 296 | overflow="hidden" |
| 297 | > |
| 298 | {addon.image ? ( |
| 299 | <img |
| 300 | src={getImageUrl(addon.image)} |
| 301 | alt={addon.title} |
| 302 | style={{ |
| 303 | width: '100%', |
| 304 | height: '100%', |
| 305 | objectFit: 'contain', |
| 306 | borderRadius: '50%', |
| 307 | }} |
| 308 | onError={(e) => { |
| 309 | e.target.style.display = 'none'; |
| 310 | if (e.target.nextSibling) { |
| 311 | e.target.nextSibling.style.display = 'flex'; |
| 312 | } |
| 313 | }} |
| 314 | /> |
| 315 | ) : null} |
| 316 | <Box |
| 317 | display={addon.image ? 'none' : 'flex'} |
| 318 | alignItems="center" |
| 319 | justifyContent="center" |
| 320 | fontSize={{ base: 'xl', sm: '2xl' }} |
| 321 | width="100%" |
| 322 | height="100%" |
| 323 | > |
| 324 | 📋 |
| 325 | </Box> |
| 326 | </Box> |
| 327 | |
| 328 | {/* Right Side - Title and Badge */} |
| 329 | <VStack |
| 330 | align="start" |
| 331 | spacing={{ base: '2', sm: '3' }} |
| 332 | flex="1" |
| 333 | minW="0" |
| 334 | > |
| 335 | <HStack justify="space-between" w="full" align="start" spacing="2"> |
| 336 | <Heading |
| 337 | size="sm" |
| 338 | color="gray.800" |
| 339 | fontWeight="600" |
| 340 | fontSize={{ base: '14px', sm: '15px', md: '16px' }} |
| 341 | noOfLines={2} |
| 342 | flex="1" |
| 343 | minW="0" |
| 344 | > |
| 345 | {addon.title} |
| 346 | </Heading> |
| 347 | <Badge |
| 348 | fontSize={badgeStyles.fontSize} |
| 349 | px="2" |
| 350 | py="1" |
| 351 | borderRadius="base" |
| 352 | bg={badgeStyles.bg} |
| 353 | border={badgeStyles.border} |
| 354 | color={badgeStyles.color} |
| 355 | fontWeight={badgeStyles.fontWeight} |
| 356 | flexShrink={0} |
| 357 | textTransform="uppercase" |
| 358 | > |
| 359 | {getPlanBadge(addon.plan)} |
| 360 | </Badge> |
| 361 | </HStack> |
| 362 | |
| 363 | <Tooltip |
| 364 | label={ |
| 365 | addon.excerpt || __('No description available.', 'everest-forms') |
| 366 | } |
| 367 | placement="bottom" |
| 368 | hasArrow |
| 369 | bg="white" |
| 370 | color="gray.800" |
| 371 | fontSize="sm" |
| 372 | fontWeight="400" |
| 373 | borderRadius="md" |
| 374 | px="3" |
| 375 | py="2" |
| 376 | > |
| 377 | <Text |
| 378 | fontSize={{ base: '12px', sm: '13px' }} |
| 379 | color="gray.600" |
| 380 | lineHeight="1.5" |
| 381 | noOfLines={2} |
| 382 | cursor="help" |
| 383 | > |
| 384 | {addon.excerpt || |
| 385 | __('No description available.', 'everest-forms')} |
| 386 | </Text> |
| 387 | </Tooltip> |
| 388 | </VStack> |
| 389 | </HStack> |
| 390 | |
| 391 | <HStack |
| 392 | justify="space-between" |
| 393 | align="center" |
| 394 | mt="auto" |
| 395 | // pt={{ base: '2', sm: '3' }} |
| 396 | pt="16px" |
| 397 | borderTop="1px solid" |
| 398 | borderColor="gray.100" |
| 399 | flexWrap={{ base: 'wrap', sm: 'nowrap' }} |
| 400 | gap={{ base: '2', sm: '0' }} |
| 401 | > |
| 402 | <HStack |
| 403 | spacing="2" |
| 404 | fontSize={{ base: '12px', sm: '13px' }} |
| 405 | flexWrap="wrap" |
| 406 | flex={{ base: '1', sm: '0 1 auto' }} |
| 407 | > |
| 408 | {addon.link && ( |
| 409 | <Link |
| 410 | href={addon.link} |
| 411 | color="gray.600" |
| 412 | textDecoration="none" |
| 413 | isExternal |
| 414 | _hover={{ color: '#7545bb', textDecoration: 'underline' }} |
| 415 | fontWeight="500" |
| 416 | > |
| 417 | {__('Docs', 'everest-forms')} |
| 418 | </Link> |
| 419 | )} |
| 420 | {addon.demo_video_url && ( |
| 421 | <> |
| 422 | <Text color="gray.300" display={{ base: 'none', sm: 'block' }}> |
| 423 | | |
| 424 | </Text> |
| 425 | <IconButton |
| 426 | size="xs" |
| 427 | icon={<Icon as={FaPlay} />} |
| 428 | aria-label={__('Video Tutorial', 'everest-forms')} |
| 429 | variant="ghost" |
| 430 | color="gray.600" |
| 431 | _hover={{ color: '#7545bb', bg: 'gray.50' }} |
| 432 | onClick={handleVideoPlay} |
| 433 | /> |
| 434 | </> |
| 435 | )} |
| 436 | {addon.setting_url && isActive && ( |
| 437 | <> |
| 438 | <Text color="gray.300" display={{ base: 'none', sm: 'block' }}> |
| 439 | | |
| 440 | </Text> |
| 441 | <IconButton |
| 442 | size="xs" |
| 443 | icon={<FaCog />} |
| 444 | aria-label={__('Settings', 'everest-forms')} |
| 445 | variant="ghost" |
| 446 | color="gray.600" |
| 447 | _hover={{ color: '#7545bb', bg: 'gray.50' }} |
| 448 | onClick={() => window.open(addon.setting_url, '_self')} |
| 449 | /> |
| 450 | </> |
| 451 | )} |
| 452 | </HStack> |
| 453 | <HStack spacing="2" flexShrink={0}> |
| 454 | {moduleEnabled ? ( |
| 455 | <Switch |
| 456 | isChecked={isActive} |
| 457 | onChange={handleToggle} |
| 458 | isDisabled={isLoading} |
| 459 | size="md" |
| 460 | sx={{ |
| 461 | '& .chakra-switch__track': { |
| 462 | bg: isActive ? '#4cc741' : 'gray.300', |
| 463 | width:"28px", |
| 464 | height:"16px", |
| 465 | p:"2px", |
| 466 | boxSizing:"border-box", |
| 467 | }, |
| 468 | '& .chakra-switch__track[data-checked]': { |
| 469 | bg: '#4cc741', |
| 470 | }, |
| 471 | '& .chakra-switch__thumb': { |
| 472 | width:"12px", |
| 473 | height:"12px", |
| 474 | }, |
| 475 | '& .chakra-switch__thumb[data-checked]': { |
| 476 | transform:"translateX(12px)", |
| 477 | }, |
| 478 | }} |
| 479 | /> |
| 480 | ) : ( |
| 481 | <Button |
| 482 | size="sm" |
| 483 | fontSize={{ base: '12px', sm: '13px' }} |
| 484 | fontWeight="600" |
| 485 | bg="#7545bb" |
| 486 | color="white" |
| 487 | borderColor="#7545bb" |
| 488 | px={{ base: '3', sm: '4' }} |
| 489 | h={{ base: '28px', sm: '32px' }} |
| 490 | _hover={{ |
| 491 | bg: '#7545bb', |
| 492 | borderColor: '#7545bb', |
| 493 | }} |
| 494 | _active={{ |
| 495 | bg: '#7545bb', |
| 496 | }} |
| 497 | onClick={handleUpgradePlan} |
| 498 | > |
| 499 | {__('Upgrade Plan', 'everest-forms')} |
| 500 | </Button> |
| 501 | )} |
| 502 | </HStack> |
| 503 | </HStack> |
| 504 | |
| 505 | {/* YouTube Video Modal */} |
| 506 | {isVideoOpen && addon.demo_video_url && ( |
| 507 | <Modal |
| 508 | isOpen={isVideoOpen} |
| 509 | onClose={onVideoClose} |
| 510 | size={{ base: 'full', sm: 'xl', md: '2xl', lg: '3xl' }} |
| 511 | isCentered |
| 512 | > |
| 513 | <ModalOverlay bg="blackAlpha.700" /> |
| 514 | <ModalContent |
| 515 | mx={{ base: '0', sm: '4' }} |
| 516 | my={{ base: '0', sm: 'auto' }} |
| 517 | > |
| 518 | <ModalHeader |
| 519 | textAlign="center" |
| 520 | fontSize={{ base: 'md', sm: 'lg' }} |
| 521 | fontWeight="600" |
| 522 | px={{ base: '4', sm: '6' }} |
| 523 | > |
| 524 | {addon.title} |
| 525 | </ModalHeader> |
| 526 | <ModalCloseButton /> |
| 527 | <ModalBody pb={{ base: '4', sm: '6' }} px={{ base: '4', sm: '6' }}> |
| 528 | <Box |
| 529 | position="relative" |
| 530 | paddingTop="56.25%" |
| 531 | bg="gray.100" |
| 532 | borderRadius="md" |
| 533 | overflow="hidden" |
| 534 | > |
| 535 | <ReactPlayer |
| 536 | url={`https://www.youtube.com/watch?v=${addon.demo_video_url}`} |
| 537 | playing={true} |
| 538 | width="100%" |
| 539 | height="100%" |
| 540 | controls |
| 541 | style={{ |
| 542 | position: 'absolute', |
| 543 | top: 0, |
| 544 | left: 0, |
| 545 | }} |
| 546 | onReady={() => setVideoLoading(false)} |
| 547 | onStart={() => setVideoLoading(false)} |
| 548 | /> |
| 549 | {videoLoading && ( |
| 550 | <Box |
| 551 | position="absolute" |
| 552 | top="50%" |
| 553 | left="50%" |
| 554 | transform="translate(-50%, -50%)" |
| 555 | > |
| 556 | <Spinner size="lg" color="#7545bb" /> |
| 557 | </Box> |
| 558 | )} |
| 559 | </Box> |
| 560 | </ModalBody> |
| 561 | </ModalContent> |
| 562 | </Modal> |
| 563 | )} |
| 564 | </Box> |
| 565 | ); |
| 566 | }; |
| 567 | |
| 568 | export default AddonCard; |
| 569 |