diff --git a/.changeset/good-pants-invite.md b/.changeset/good-pants-invite.md new file mode 100644 index 00000000..e7ebeae2 --- /dev/null +++ b/.changeset/good-pants-invite.md @@ -0,0 +1,5 @@ +--- +"strapi-plugin-webtools": patch +--- + +Feature/try webtools inscentives diff --git a/CLAUDE.md b/CLAUDE.md index 2101002d..e1b5b759 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,6 +51,16 @@ Builds all packages using Turborepo's caching and dependency graph. ## Testing +### Node version + +Always activate Node 20 before running yarn commands: + +```bash +nvm use # reads .nvmrc (Node 20) +``` + +The repo uses **Yarn 4.11.0** via `yarnPath` in `.yarnrc.yml`. Running `yarn` anywhere inside the repo automatically uses this version — even in subdirectories like `playground/`. + ### Unit Tests ```bash @@ -69,6 +79,8 @@ Unit tests are located in `__tests__` directories: Uses Jest with ts-jest preset. Test files: `*.test.ts` or `*.test.js` +`playground/.env` is **force-committed** to git with placeholder values (`tobemodified`) so CI can boot Strapi. Do not add real secrets to it — use your local `.env` override for `WEBTOOLS_LICENSE_KEY` and similar. + ### Integration Tests ```bash @@ -184,6 +196,10 @@ Addons are Strapi plugins with a special flag in their `package.json`: **Discovery**: Core scans `enabledPlugins` at runtime for this flag +**Addon detection in admin UI**: Always match on `addon.info.name` (the Strapi plugin name, e.g. `webtools-addon-redirects`), **not** on UI labels which are translatable. Strip the npm scope before comparing: `@pluginpal/webtools-addon-redirects` → `webtools-addon-redirects`. + +**Pro addons** (not open-source) are defined in `admin/constants/pro-addons.ts`. When not installed, they appear in the sidebar as locked items (`LockedAddonMenuItem`) that open a `TrialModal`. The `packageName` field in `ProAddon` is the full scoped npm name. + **Integration**: Addons inject components via named zones: - `webtoolsRouter`: Adds routes to main navigation - `webtoolsSidePanel`: Adds components to content editor sidebar @@ -296,6 +312,23 @@ See `packages/addons/sitemap` as reference: ``` 4. Optionally extend content types in server `bootstrap()` +## ESLint rules to watch + +- **`max-len`**: 100 character limit. Long inline comments will fail. +- **`no-confusing-arrow` + `implicit-arrow-linebreak`**: Arrow functions with ternaries must use a block body: + ```typescript + // ✗ fails linting + const fn = (x: string) => + x.includes('/') ? x.split('/')[1] : x; + + // ✓ correct + const fn = (x: string) => { + if (x.includes('/')) return x.split('/')[1]; + return x; + }; + ``` +- **`@typescript-eslint/no-unnecessary-type-assertion`**: Remove casts that TypeScript already infers. + ## Release Process This project uses Changesets for version management: diff --git a/packages/core/admin/components/LockedAddonMenuItem/index.tsx b/packages/core/admin/components/LockedAddonMenuItem/index.tsx new file mode 100644 index 00000000..ecb1cbf9 --- /dev/null +++ b/packages/core/admin/components/LockedAddonMenuItem/index.tsx @@ -0,0 +1,52 @@ +import React, { useState } from 'react'; +import { useIntl } from 'react-intl'; +import { + SubNavLink, + Tooltip, +} from '@strapi/design-system'; +import { Lock } from '@strapi/icons'; +import { LockedAddonMenuItemProps } from '../../types/pro-addons'; +import TrialModal from '../TrialModal'; + +const LockedAddonMenuItem: React.FC = ({ addon }) => { + const { formatMessage } = useIntl(); + const [isModalOpen, setIsModalOpen] = useState(false); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + setIsModalOpen(true); + }; + + return ( + <> + + + + + {addon.name} + + + + + setIsModalOpen(false)} + /> + + ); +}; + +export default LockedAddonMenuItem; diff --git a/packages/core/admin/components/TrialCallToAction/index.tsx b/packages/core/admin/components/TrialCallToAction/index.tsx new file mode 100644 index 00000000..37199e49 --- /dev/null +++ b/packages/core/admin/components/TrialCallToAction/index.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { useIntl } from 'react-intl'; +import { + Box, + Flex, + Typography, + Button, +} from '@strapi/design-system'; +import { TRIAL_URL } from '../../constants/pro-addons'; + +interface TrialCallToActionProps { + variant?: 'banner' | 'card' | 'inline'; +} + +const TrialCallToAction: React.FC = ({ variant = 'card' }) => { + const { formatMessage } = useIntl(); + + const content = ( + + + {formatMessage({ + id: 'webtools.overview.trial_cta.title_short', + defaultMessage: 'Ready to unlock Pro features?', + })} + + + + {formatMessage({ + id: 'webtools.overview.trial_cta.subtitle', + defaultMessage: 'Start your free 7-day trial - includes Redirects & Links addons', + })} + + + + + + + ); + + if (variant === 'inline') { + return content; + } + + return ( + + {content} + + ); +}; + +export default TrialCallToAction; diff --git a/packages/core/admin/components/TrialModal/index.tsx b/packages/core/admin/components/TrialModal/index.tsx new file mode 100644 index 00000000..34815947 --- /dev/null +++ b/packages/core/admin/components/TrialModal/index.tsx @@ -0,0 +1,238 @@ +import React from 'react'; +import { useIntl } from 'react-intl'; +import { + Modal, + Flex, + Typography, + Button, + Box, + Badge, + Grid, +} from '@strapi/design-system'; +import { + ArrowRight, + Link as LinkIcon, + ExternalLink, +} from '@strapi/icons'; +import { TrialModalProps } from '../../types/pro-addons'; +import { TRIAL_URL } from '../../constants/pro-addons'; + +const iconMap = { + ArrowRight, + Link: LinkIcon, + ArrowsLeftRight: ArrowRight, +}; + +const TrialModal: React.FC = ({ addon, isOpen, onClose }) => { + const { formatMessage } = useIntl(); + const IconComponent = iconMap[addon.icon as keyof typeof iconMap] || ArrowRight; + + if (!isOpen) return null; + + return ( + + + + + + + + + + + {formatMessage({ + id: 'webtools.trial_modal.title', + defaultMessage: 'Unlock {name}', + }, { name: addon.name })} + + + {formatMessage({ + id: 'webtools.overview.addon.pro', + defaultMessage: 'PRO', + })} + + + + {addon.tagline} + + + + + + + + {/* Two column layout: Key Benefits | Trial Details */} + + {/* Left column: Key Benefits */} + + + + {formatMessage({ + id: 'webtools.trial_modal.benefits_title', + defaultMessage: 'Key Benefits', + })} + + + {addon.benefits.map((benefit) => ( + + + + • + + + + {benefit} + + + ))} + + + + + {/* Right column: Trial Details */} + + + + {formatMessage({ + id: 'webtools.trial_modal.trial_details_title', + defaultMessage: 'Trial Details', + })} + + + + + + ✓ + + + + {formatMessage({ + id: 'webtools.trial_modal.trial_detail_1', + defaultMessage: '7-day free trial', + })} + + + + + + ✓ + + + + {formatMessage({ + id: 'webtools.trial_modal.trial_detail_2', + defaultMessage: 'Includes Redirects + Links addons', + })} + + + + + + ✓ + + + + {formatMessage({ + id: 'webtools.trial_modal.trial_detail_3', + defaultMessage: 'No credit card required', + })} + + + + + + ✓ + + + + {formatMessage({ + id: 'webtools.trial_modal.trial_detail_4', + defaultMessage: 'Cancel anytime', + })} + + + + + + + + {/* Testimonial */} + + + {formatMessage({ + id: 'webtools.trial_modal.testimonial', + defaultMessage: '"Saved us 20+ hours per project. Essential for our agency workflow."', + })} + + + {formatMessage({ + id: 'webtools.trial_modal.testimonial_author', + defaultMessage: '— Marcus, Digital Agency Owner', + })} + + + + + + + + + + + + + + ); +}; + +export default TrialModal; diff --git a/packages/core/admin/constants/pro-addons.ts b/packages/core/admin/constants/pro-addons.ts new file mode 100644 index 00000000..6857baf5 --- /dev/null +++ b/packages/core/admin/constants/pro-addons.ts @@ -0,0 +1,55 @@ +import { ProAddon } from '../types/pro-addons'; + +export const PRO_ADDONS: ProAddon[] = [ + { + id: 'redirects', + name: 'Redirects', + packageName: '@pluginpal/webtools-addon-redirects', + tagline: 'Never lose SEO value when URLs change', + description: 'Automatically manage redirects when URL aliases change. Prevent broken links and maintain SEO rankings.', + benefits: [ + 'Save 30+ hours/month on manual redirect management', + 'Chain & loop detection prevents redirect errors', + 'Preserve search rankings with automatic 301s', + 'REST API for frontend integration', + ], + icon: 'ArrowRight', + docsUrl: 'https://docs.pluginpal.io/webtools/addons/redirects', + value: 'Maintains SEO rankings during site reorganizations', + }, + { + id: 'links', + name: 'Links', + packageName: '@pluginpal/webtools-addon-links', + tagline: 'Smart internal linking for your content', + description: 'Custom field type for creating internal links that persist across URL changes. Links survive URL updates automatically.', + benefits: [ + 'Content teams manage links without developer help', + 'Links survive URL changes (document ID based)', + 'CKEditor & Magic Editor integration', + 'Smart search finds content instantly', + ], + icon: 'Link', + docsUrl: 'https://docs.pluginpal.io/webtools/addons/links', + value: 'Empowers content teams, reduces broken internal links', + }, + { + id: 'breadcrumbs', + name: 'Breadcrumbs', + packageName: '@pluginpal/webtools-addon-breadcrumbs', + tagline: 'Automated breadcrumb navigation', + description: 'Generate breadcrumb trails automatically based on URL structure. Zero manual maintenance required.', + benefits: [ + 'Automatic generation from URL structure', + 'Multilingual breadcrumbs out-of-the-box', + 'Works as API relation (populate in queries)', + 'Customizable per content type', + ], + icon: 'ArrowsLeftRight', + docsUrl: 'https://docs.pluginpal.io/webtools/addons/breadcrumbs', + value: 'Improves UX and SEO without developer maintenance', + }, +]; + +export const TRIAL_URL = 'https://polar.sh/checkout/polar_c_2scL6ja7SEWFHarsc21Pc8fHhRy14ib6XGUol2V5f7r'; +export const DOCS_URL = 'https://docs.pluginpal.io/webtools/'; diff --git a/packages/core/admin/containers/App/index.tsx b/packages/core/admin/containers/App/index.tsx index ed6262d8..e4d20f67 100644 --- a/packages/core/admin/containers/App/index.tsx +++ b/packages/core/admin/containers/App/index.tsx @@ -18,7 +18,9 @@ import { useStrapiApp, Layouts, useRBAC, + getFetchClient, } from '@strapi/strapi/admin'; +import { useQuery } from 'react-query'; import pluginPermissions from '../../permissions'; import pluginId from '../../helpers/pluginId'; @@ -29,6 +31,9 @@ import PatternsEditPage from '../../screens/Patterns/EditPage'; import PatternsCreatePage from '../../screens/Patterns/CreatePage'; import PageNotFound from '../../screens/404'; import { InjectedRoute } from '../../types/injection-zones'; +import { WebtoolsAddonInfo } from '../../types/addons'; +import { PRO_ADDONS } from '../../constants/pro-addons'; +import LockedAddonMenuItem from '../../components/LockedAddonMenuItem'; const App = () => { const getPlugin = useStrapiApp('MyComponent', (state) => state.getPlugin); @@ -44,6 +49,23 @@ const App = () => { const location = useLocation(); const currentPath = location.pathname; + const { get } = getFetchClient(); + const addonsQuery = useQuery('addons', async () => get('/webtools/info/addons')); + + const installedPluginNames = Object.values(addonsQuery.data?.data || {}) + .map((addon) => addon.info.name); + + // Strip npm scope: "@pluginpal/webtools-addon-redirects" → "webtools-addon-redirects" + const getPluginName = (packageName: string) => { + if (packageName.includes('/')) return packageName.split('/')[1]; + return packageName; + }; + + // Find locked addons (Pro addons that are NOT installed) + const lockedAddons = PRO_ADDONS.filter( + (addon) => !installedPluginNames.includes(getPluginName(addon.packageName)), + ); + return ( { )} - {routerComponents.length > 0 && ( + {(routerComponents.length > 0 || lockedAddons.length > 0) && ( + {/* Installed addons - existing functionality */} {routerComponents.map(({ path, label }) => label && ( {label} ))} + + {/* Locked Pro addons - new functionality */} + {lockedAddons.map((addon) => ( + + ))} )} diff --git a/packages/core/admin/screens/Overview/components/AddonsList/index.tsx b/packages/core/admin/screens/Overview/components/AddonsList/index.tsx new file mode 100644 index 00000000..8f9a6174 --- /dev/null +++ b/packages/core/admin/screens/Overview/components/AddonsList/index.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { useIntl } from 'react-intl'; +import { + Card, + CardBody, + CardContent, + CardTitle, + Box, + Flex, + Badge, + Grid, + Typography, +} from '@strapi/design-system'; +import { PuzzlePiece, Check } from '@strapi/icons'; +import { WebtoolsAddonInfo } from '../../../../types/addons'; + +interface AddonsListProps { + addons: WebtoolsAddonInfo[]; +} + +const AddonsList: React.FC = ({ addons }) => { + const { formatMessage } = useIntl(); + + return ( + + {addons.map((addon) => ( + + + + + + + + + + + {formatMessage({ + id: 'webtools.overview.addon.active', + defaultMessage: 'Active', + })} + + + + + + {addon.info.addonName} + + {addon.info.description} + + + + + + + ))} + + ); +}; + +export default AddonsList; diff --git a/packages/core/admin/screens/Overview/components/HeroSection/index.tsx b/packages/core/admin/screens/Overview/components/HeroSection/index.tsx new file mode 100644 index 00000000..266ac5c8 --- /dev/null +++ b/packages/core/admin/screens/Overview/components/HeroSection/index.tsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { useIntl } from 'react-intl'; +import { + Box, + Flex, + Typography, + Button, +} from '@strapi/design-system'; +import { ExternalLink } from '@strapi/icons'; +import { TRIAL_URL, DOCS_URL } from '../../../../constants/pro-addons'; +import packageJson from '../../../../../package.json'; + +const HeroSection = () => { + const { formatMessage } = useIntl(); + + return ( + + + + {formatMessage({ + id: 'webtools.overview.hero.title', + defaultMessage: 'Welcome to Webtools {version}', + }, { version: `v${packageJson.version}` })} + + + + {formatMessage({ + id: 'webtools.overview.hero.subtitle', + defaultMessage: 'Everything you need to build professional websites with Strapi CMS', + })} + + + + + + + + + ); +}; + +export default HeroSection; diff --git a/packages/core/admin/screens/Overview/components/ProAddonCard/index.tsx b/packages/core/admin/screens/Overview/components/ProAddonCard/index.tsx new file mode 100644 index 00000000..c5a526ac --- /dev/null +++ b/packages/core/admin/screens/Overview/components/ProAddonCard/index.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { useIntl } from 'react-intl'; +import { + Card, + CardBody, + CardContent, + CardTitle, + Box, + Flex, + Typography, + Button, + Badge, +} from '@strapi/design-system'; +import { + ArrowRight, + Link as LinkIcon, + Check, +} from '@strapi/icons'; +import { ProAddonCardProps } from '../../../../types/pro-addons'; +import { TRIAL_URL } from '../../../../constants/pro-addons'; + +const iconMap = { + ArrowRight, + Link: LinkIcon, + ArrowsLeftRight: ArrowRight, +}; + +const ProAddonCard: React.FC = ({ addon, isInstalled }) => { + const { formatMessage } = useIntl(); + const IconComponent = iconMap[addon.icon as keyof typeof iconMap] || ArrowRight; + + const handleCardClick = () => { + if (!isInstalled) { + window.open(TRIAL_URL, '_blank', 'noopener,noreferrer'); + } + }; + + return ( + ) => { + if (!isInstalled) { + e.currentTarget.style.transform = 'translateY(-2px)'; + e.currentTarget.style.boxShadow = '0 4px 12px rgba(0, 0, 0, 0.1)'; + } + }} + onMouseLeave={(e: React.MouseEvent) => { + e.currentTarget.style.transform = 'translateY(0)'; + e.currentTarget.style.boxShadow = ''; + }} + onClick={handleCardClick} + > + + + + + + {isInstalled ? ( + + + + {formatMessage({ + id: 'webtools.overview.addon.active', + defaultMessage: 'Active', + })} + + + ) : ( + + {formatMessage({ + id: 'webtools.overview.addon.pro', + defaultMessage: 'PRO', + })} + + )} + + + + + {addon.name} + + + {addon.tagline} + + + + + {addon.value} + + + + {!isInstalled && ( + + + + )} + + + + + ); +}; + +export default ProAddonCard; diff --git a/packages/core/admin/screens/Overview/index.tsx b/packages/core/admin/screens/Overview/index.tsx index 70da55bb..1fe88c6b 100644 --- a/packages/core/admin/screens/Overview/index.tsx +++ b/packages/core/admin/screens/Overview/index.tsx @@ -1,17 +1,24 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useIntl } from 'react-intl'; import isEmpty from 'lodash/isEmpty'; import { - Typography, Grid, Flex, Link, + Typography, + Grid, + Flex, + Link, Card, CardBody, CardTitle, CardSubtitle, CardContent, Box, + Badge, + Button, } from '@strapi/design-system'; -import { ExternalLink, PuzzlePiece } from '@strapi/icons'; +import { + ExternalLink, PuzzlePiece, Lock, Check, +} from '@strapi/icons'; import { Page, getFetchClient, Layouts } from '@strapi/strapi/admin'; import { useQuery } from 'react-query'; @@ -20,12 +27,15 @@ import { WebtoolsAddonInfo } from '../../types/addons'; import packageJson from '../../../package.json'; import { EnabledContentTypes } from '../../types/enabled-contenttypes'; import ContentTypesList from './components/ContentTypesList'; +import { PRO_ADDONS, TRIAL_URL } from '../../constants/pro-addons'; +import TrialModal from '../../components/TrialModal'; const List = () => { const { get } = getFetchClient(); const addons = useQuery('addons', async () => get('/webtools/info/addons')); const contentTypes = useQuery('content-types', async () => get('/webtools/info/getContentTypes')); const { formatMessage } = useIntl(); + const [selectedAddon, setSelectedAddon] = useState(null); if (addons.isLoading || contentTypes.isLoading) { return ( @@ -39,6 +49,43 @@ const List = () => { ); } + const installedAddons = Object.values(addons.data.data || {}); + + // Strip npm scope: "@pluginpal/webtools-addon-redirects" → "webtools-addon-redirects" + const getPluginName = (packageName: string) => { + if (packageName.includes('/')) return packageName.split('/')[1]; + return packageName; + }; + + const installedPluginNames = installedAddons.map((addon) => addon.info.name); + + const isAddonInstalled = (packageName: string): boolean => { + return installedPluginNames.includes(getPluginName(packageName)); + }; + + // Only show locked Pro addons that are NOT installed + const lockedProAddons = PRO_ADDONS.filter((proAddon) => !isAddonInstalled(proAddon.packageName)); + + // Check if user has Pro license (at least one Pro addon installed) + const hasProLicense = PRO_ADDONS.some((proAddon) => isAddonInstalled(proAddon.packageName)); + + // Combine installed and locked pro addons + const allAddonsToShow = [ + ...installedAddons.map((addon) => ({ + type: 'installed' as const, + info: addon.info, + })), + ...lockedProAddons.map((proAddon) => ({ + type: 'locked' as const, + info: { + name: proAddon.packageName, + addonName: proAddon.name, + description: proAddon.description, + }, + proAddon, + })), + ]; + return ( { defaultMessage: 'Webtools version', })} - + v{packageJson.version} + {!hasProLicense && ( + + )} @@ -122,7 +184,7 @@ const List = () => { - {!isEmpty(addons.data.data) && ( + {!isEmpty(allAddonsToShow) && ( { }, )} - - {Object.values(addons.data.data).map((addon) => ( - - - - - - - {addon.info.addonName} - {addon.info.description} - - - - ))} - + + {allAddonsToShow.map((addon) => { + const isLocked = addon.type === 'locked'; + + return ( + setSelectedAddon(addon.proAddon) + : undefined + } + > + + {/* Badge positioned absolutely in top-right */} + + {isLocked ? ( + + {formatMessage({ + id: 'webtools.overview.addon.pro', + defaultMessage: 'PRO', + })} + + ) : ( + + + + {formatMessage({ + id: 'webtools.overview.addon.active', + defaultMessage: 'Active', + })} + + + )} + + + {/* Icon */} + + {isLocked ? : } + + + {/* Content with padding-right to prevent badge overlap */} + + + {addon.info.addonName} + + + {addon.info.description} + + + + + ); + })} + )} + + {selectedAddon && ( + setSelectedAddon(null)} + /> + )} ); }; diff --git a/packages/core/admin/translations/en.json b/packages/core/admin/translations/en.json index 781e05ac..630c3f05 100644 --- a/packages/core/admin/translations/en.json +++ b/packages/core/admin/translations/en.json @@ -68,5 +68,42 @@ "settings.page.patterns.create.subtitle": "Pattern details", "settings.page.patterns.create.description": "Add a pattern for automatic URL alias generation.", - "notification.success.permalink_copied": "Permalink copied to the clipboard" + "notification.success.permalink_copied": "Permalink copied to the clipboard", + + "overview.hero.title": "Welcome to Webtools {version}", + "overview.hero.subtitle": "Everything you need to build professional websites with Strapi CMS", + "overview.hero.trial_button_short": "Start Free Trial", + "overview.hero.docs_button": "View Documentation", + + "overview.installed_addons.title": "Your Addons", + "overview.installed_addons.description": "Manage your installed addons", + "overview.installed_addons.empty": "No addons installed yet. Start with a free trial!", + + "overview.pro_addons.title": "Unlock More with Pro Addons", + "overview.pro_addons.description": "Extend Webtools with powerful premium features", + + "overview.addon.active": "Active", + "overview.addon.pro": "PRO", + "overview.addon.learn_more": "Learn More", + "overview.addon.start_trial": "Start Free Trial", + + "overview.try_premium": "Try Premium Free", + + "overview.trial_cta.title_short": "Ready to unlock Pro features?", + "overview.trial_cta.subtitle": "Start your free 7-day trial - includes Redirects & Links addons", + "overview.trial_cta.button_short": "Start Free Trial", + + "trial_modal.title": "Unlock {name}", + "trial_modal.benefits_title": "KEY BENEFITS", + "trial_modal.testimonial": "\"Saved us 20+ hours per project. Essential for our agency workflow.\"", + "trial_modal.testimonial_author": "— Marcus, Digital Agency Owner", + "trial_modal.trial_details_title": "TRIAL DETAILS", + "trial_modal.trial_detail_1": "✅ 7-day free trial", + "trial_modal.trial_detail_2": "✅ Includes Redirects + Links addons", + "trial_modal.trial_detail_3": "✅ No credit card required", + "trial_modal.trial_detail_4": "✅ Cancel anytime", + "trial_modal.learn_more": "Learn More", + "trial_modal.start_trial": "Start Free Trial", + + "sidebar.locked_addon.tooltip": "Start free trial to unlock" } diff --git a/packages/core/admin/translations/nl.json b/packages/core/admin/translations/nl.json index 5da8ace9..999bf5ca 100644 --- a/packages/core/admin/translations/nl.json +++ b/packages/core/admin/translations/nl.json @@ -66,5 +66,42 @@ "settings.page.patterns.create.subtitle": "Patroondetails", "settings.page.patterns.create.description": "Voeg een patroon toe voor het automatisch genereren van URL-aliassen.", - "notification.success.permalink_copied": "Permalink gekopieerd naar het klembord" + "notification.success.permalink_copied": "Permalink gekopieerd naar het klembord", + + "overview.hero.title": "Welkom bij Webtools {version}", + "overview.hero.subtitle": "Alles wat je nodig hebt om professionele websites te bouwen met Strapi CMS", + "overview.hero.trial_button_short": "Start Gratis Proefperiode", + "overview.hero.docs_button": "Bekijk Documentatie", + + "overview.installed_addons.title": "Jouw Addons", + "overview.installed_addons.description": "Beheer je geïnstalleerde addons", + "overview.installed_addons.empty": "Nog geen addons geïnstalleerd. Start met een gratis proefperiode!", + + "overview.pro_addons.title": "Ontgrendel Meer met Pro Addons", + "overview.pro_addons.description": "Breid Webtools uit met krachtige premium functies", + + "overview.addon.active": "Actief", + "overview.addon.pro": "PRO", + "overview.addon.learn_more": "Meer Informatie", + "overview.addon.start_trial": "Start Gratis Proefperiode", + + "overview.try_premium": "Probeer Premium Gratis", + + "overview.trial_cta.title_short": "Klaar om Pro functies te ontgrendelen?", + "overview.trial_cta.subtitle": "Start je gratis 7-daagse proefperiode - inclusief Redirects & Links addons", + "overview.trial_cta.button_short": "Start Gratis Proefperiode", + + "trial_modal.title": "Ontgrendel {name}", + "trial_modal.benefits_title": "BELANGRIJKSTE VOORDELEN", + "trial_modal.testimonial": "\"Bespaarde ons 20+ uur per project. Essentieel voor onze agency workflow.\"", + "trial_modal.testimonial_author": "— Marcus, Eigenaar Digitaal Bureau", + "trial_modal.trial_details_title": "PROEFPERIODE DETAILS", + "trial_modal.trial_detail_1": "✅ 7-daagse gratis proefperiode", + "trial_modal.trial_detail_2": "✅ Inclusief Redirects + Links addons", + "trial_modal.trial_detail_3": "✅ Geen creditcard vereist", + "trial_modal.trial_detail_4": "✅ Altijd opzegbaar", + "trial_modal.learn_more": "Meer Informatie", + "trial_modal.start_trial": "Start Gratis Proefperiode", + + "sidebar.locked_addon.tooltip": "Start gratis proefperiode om te ontgrendelen" } diff --git a/packages/core/admin/types/pro-addons.ts b/packages/core/admin/types/pro-addons.ts new file mode 100644 index 00000000..a3d09717 --- /dev/null +++ b/packages/core/admin/types/pro-addons.ts @@ -0,0 +1,26 @@ +export interface ProAddon { + id: string; + name: string; + packageName: string; + tagline: string; + description: string; + benefits: string[]; + icon: string; + docsUrl: string; + value: string; +} + +export interface ProAddonCardProps { + addon: ProAddon; + isInstalled: boolean; +} + +export interface LockedAddonMenuItemProps { + addon: ProAddon; +} + +export interface TrialModalProps { + addon: ProAddon; + isOpen: boolean; + onClose: () => void; +} diff --git a/playground/.env b/playground/.env index 4a82be43..f82995af 100644 --- a/playground/.env +++ b/playground/.env @@ -1,4 +1,3 @@ - HOST=0.0.0.0 PORT=1337 APP_KEYS="toBeModified1,toBeModified2" diff --git a/playground/.env.example b/playground/.env.example index ebfc96a3..3aa37660 100644 --- a/playground/.env.example +++ b/playground/.env.example @@ -5,3 +5,6 @@ API_TOKEN_SALT=tobemodified ADMIN_JWT_SECRET=tobemodified TRANSFER_TOKEN_SALT=tobemodified JWT_SECRET=tobemodified + +# PluginPal Webtools license key (required for pro features) +WEBTOOLS_LICENSE_KEY= diff --git a/playground/package.json b/playground/package.json index 81745821..9a551a62 100644 --- a/playground/package.json +++ b/playground/package.json @@ -36,6 +36,9 @@ "npm": ">=6.0.0" }, "strapi": { - "uuid": "90b22b41-262b-40ae-9b68-3a3e507df1b9" + "uuid": "90b22b41-262b-40ae-9b68-3a3e507df1b9", + "webtools": { + "activation_id": "044cbffa-b3f1-4aab-86f6-8130089abf8e" + } } } diff --git a/playground/src/admin/app.tsx b/playground/src/admin/app.tsx new file mode 100644 index 00000000..ff8b4c56 --- /dev/null +++ b/playground/src/admin/app.tsx @@ -0,0 +1 @@ +export default {};