| @@ -1,178 +1,238 @@ | ||
| 1 | 1 | import { __, _x } from '@wordpress/i18n' |
| 2 | 2 | import classnames from 'classnames' |
| 3 | -import React, { useState } from 'react' | |
| 3 | +import React, { useMemo, useState } from 'react' | |
| 4 | 4 | import { isLicensed, shouldShowUpsell } from '../../utils/screen' |
| 5 | -import { buildUrl, fetchQueryParam } from '../../utils/urls' | |
| 6 | -import { BlueprintIcon, CommunityIcon, LibraryIcon, SettingsIcon, SnippetsIcon } from './icons/ToolbarIcons' | |
| 5 | +import { buildUrl, fetchConstQueryParam } from '../../utils/urls' | |
| 6 | +import { SUBPAGES, SUBPAGE_ENTRIES, type SubpageName } from '../ManageMenu/subpages' | |
| 7 | +import { hasSeenDemo } from './demo/useDemoSeen' | |
| 8 | +import { AiAgentIcon } from './icons/AiAgentIcon' | |
| 9 | +import { BlueprintIcon } from './icons/BlueprintIcon' | |
| 10 | +import { CommunityIcon } from './icons/CommunityIcon' | |
| 11 | +import { LibraryIcon } from './icons/LibraryIcon' | |
| 12 | +import { SettingsIcon } from './icons/SettingsIcon' | |
| 13 | +import { SnippetsIcon } from './icons/SnippetsIcon' | |
| 7 | 14 | import { UpsellDialog } from './UpsellDialog' |
| 8 | -import type { ReactNode } from 'react' | |
| 15 | +import type { SVGProps } from 'react' | |
| 9 | 16 | |
| 10 | -export const SUBPAGES = ['snippets', 'blueprints', 'cloud-community', 'cloud-library'] as const | |
| 17 | +const searchParams = new URLSearchParams(window.location.search) | |
| 11 | 18 | |
| 12 | -interface NavLink { | |
| 19 | +const managePageSlug = window.CODE_SNIPPETS?.urls.manage | |
| 20 | + ? new URL(window.CODE_SNIPPETS.urls.manage).searchParams.get('page') | |
| 21 | + : null | |
| 22 | + | |
| 23 | +// The manage screen resolves an absent or unrecognised `subpage` to the first subpage, so | |
| 24 | +// mirror that fallback here — comparing against the raw param would leave every lower-nav | |
| 25 | +// tab inactive on the default Snippets view, which is where the page opens. | |
| 26 | +const activeSubpage = searchParams.get('page') === managePageSlug | |
| 27 | + ? fetchConstQueryParam('subpage', SUBPAGES) ?? SUBPAGES[0] | |
| 28 | + : null | |
| 29 | + | |
| 30 | +interface UpperNavItemProps { | |
| 13 | 31 | name: string |
| 14 | - url?: string | |
| 32 | + url: string | |
| 15 | 33 | label: string |
| 16 | - external?: boolean | |
| 17 | - icon?: ReactNode | |
| 18 | - pro?: boolean | |
| 19 | - pageSlug?: string | |
| 20 | - subpage?: typeof SUBPAGES[number] | |
| 21 | - end?: boolean | |
| 22 | 34 | } |
| 23 | 35 | |
| 24 | -const UPPER_NAV_LINKS: readonly NavLink[] = [ | |
| 25 | - { | |
| 26 | - name: 'docs', | |
| 27 | - url: 'https://codesnippets.pro/docs', | |
| 28 | - label: __('Docs', 'code-snippets'), | |
| 29 | - external: true | |
| 30 | - }, | |
| 31 | - { | |
| 32 | - name: 'cloud', | |
| 33 | - url: 'https://codesnippets.cloud/', | |
| 34 | - label: __('Cloud Dashboard', 'code-snippets'), | |
| 35 | - external: true | |
| 36 | - }, | |
| 37 | - { | |
| 38 | - name: 'welcome', | |
| 39 | - url: window.CODE_SNIPPETS?.urls.welcome, | |
| 40 | - label: __("What's New", 'code-snippets'), | |
| 41 | - pageSlug: 'code-snippets-welcome' | |
| 42 | - }, | |
| 43 | - { | |
| 44 | - name: 'import-snippets', | |
| 45 | - url: window.CODE_SNIPPETS?.urls.import, | |
| 46 | - label: _x('Import', 'snippets', 'code-snippets'), | |
| 47 | - pageSlug: 'import-code-snippets' | |
| 48 | - } | |
| 49 | -] | |
| 36 | +const UpperNavItem = ({ name, url, label }: UpperNavItemProps) => { | |
| 37 | + const pageSlug = useMemo(() => new URL(url).searchParams.get('page'), [url]) | |
| 50 | 38 | |
| 51 | -const LOWER_NAV_LINKS: readonly NavLink[] = [ | |
| 52 | - { | |
| 53 | - name: 'snippets', | |
| 54 | - url: window.CODE_SNIPPETS?.urls.manage, | |
| 55 | - label: __('Snippets', 'code-snippets'), | |
| 56 | - icon: <SnippetsIcon aria-hidden="true" />, | |
| 57 | - pageSlug: 'snippets' | |
| 58 | - }, | |
| 59 | - { | |
| 60 | - name: 'cloud-community', | |
| 61 | - label: __('Community Cloud', 'code-snippets'), | |
| 62 | - icon: <CommunityIcon aria-hidden="true" />, | |
| 63 | - subpage: 'cloud-community' | |
| 64 | - }, | |
| 65 | - { | |
| 66 | - name: 'cloud-library', | |
| 67 | - label: __('My Library', 'code-snippets'), | |
| 68 | - icon: <LibraryIcon aria-hidden="true" />, | |
| 69 | - pro: true, | |
| 70 | - subpage: 'cloud-library' | |
| 71 | - }, | |
| 72 | - { | |
| 73 | - name: 'blueprints', | |
| 74 | - label: __('Blueprints', 'code-snippets'), | |
| 75 | - icon: <BlueprintIcon />, | |
| 76 | - pro: true, | |
| 77 | - subpage: 'blueprints' | |
| 78 | - }, | |
| 79 | - { | |
| 80 | - name: 'settings', | |
| 81 | - url: window.CODE_SNIPPETS?.urls.settings, | |
| 82 | - label: __('Settings', 'code-snippets'), | |
| 83 | - icon: <SettingsIcon aria-hidden="true" />, | |
| 84 | - pageSlug: 'snippets-settings', | |
| 85 | - end: true | |
| 86 | - } | |
| 87 | -] | |
| 39 | + const isActive = !searchParams.get('subpage') && searchParams.get('page') === pageSlug | |
| 88 | 40 | |
| 89 | -interface UpperNavProps { | |
| 90 | - setIsUpsellDialogOpen: (isOpen: boolean) => void | |
| 41 | + return ( | |
| 42 | + <li> | |
| 43 | + <a | |
| 44 | + href={url} | |
| 45 | + className={classnames(`${name}-link`, { 'active-link': isActive })} | |
| 46 | + {...!pageSlug && { target: '_blank', rel: 'noopener noreferrer' }} | |
| 47 | + > | |
| 48 | + {label} | |
| 49 | + </a> | |
| 50 | + </li> | |
| 51 | + ) | |
| 91 | 52 | } |
| 53 | +const UpperNavItems = () => | |
| 54 | + <> | |
| 55 | + {window.CODE_SNIPPETS?.urls.insights && ( | |
| 56 | + <UpperNavItem | |
| 57 | + name="insights" | |
| 58 | + url={window.CODE_SNIPPETS.urls.insights} | |
| 59 | + label={__('Insights', 'code-snippets')} | |
| 60 | + />)} | |
| 92 | 61 | |
| 93 | -const UpperNav: React.FC<UpperNavProps> = ({ setIsUpsellDialogOpen }) => | |
| 94 | - <div className="code-snippets-toolbar-upper"> | |
| 95 | - <div className="logo"> | |
| 96 | - <img | |
| 97 | - src={`${window.CODE_SNIPPETS?.urls.plugin}/assets/icon.svg`} | |
| 98 | - alt={__('Code Snippets logo', 'code-snippets')} | |
| 99 | - aria-hidden="true" | |
| 100 | - /> | |
| 62 | + {window.CODE_SNIPPETS?.urls.import && ( | |
| 63 | + <UpperNavItem | |
| 64 | + name="import-snippets" | |
| 65 | + url={window.CODE_SNIPPETS.urls.import} | |
| 66 | + label={_x('Import', 'snippets', 'code-snippets')} | |
| 67 | + />)} | |
| 101 | 68 | |
| 102 | - <div>{__('Code Snippets', 'code-snippets')}</div> | |
| 103 | - </div> | |
| 69 | + {window.CODE_SNIPPETS?.urls.welcome && ( | |
| 70 | + <UpperNavItem | |
| 71 | + name="welcome" | |
| 72 | + url={window.CODE_SNIPPETS.urls.welcome} | |
| 73 | + label={__("What's New", 'code-snippets')} | |
| 74 | + />)} | |
| 104 | 75 | |
| 105 | - <nav aria-label={__('Main links', 'code-snippets')}> | |
| 76 | + <UpperNavItem | |
| 77 | + name="docs" | |
| 78 | + url="https://codesnippets.pro/docs" | |
| 79 | + label={__('Docs', 'code-snippets')} | |
| 80 | + /> | |
| 81 | + | |
| 82 | + <UpperNavItem | |
| 83 | + name="cloud" | |
| 84 | + url={window.CODE_SNIPPETS?.urls.cloud ?? 'https://codesnippets.cloud'} | |
| 85 | + label={__('Cloud Dashboard', 'code-snippets')} | |
| 86 | + /> | |
| 87 | + | |
| 88 | + </> | |
| 89 | + | |
| 90 | +const MoreNav = () => | |
| 91 | + <li className="toolbar-more-item"> | |
| 92 | + <details className="toolbar-more-menu"> | |
| 93 | + <summary> | |
| 94 | + {__('More', 'code-snippets')} | |
| 95 | + <span className="dashicons dashicons-arrow-down-alt2" aria-hidden="true"></span> | |
| 96 | + </summary> | |
| 106 | 97 | <ul> |
| 107 | - {UPPER_NAV_LINKS.map(link => | |
| 108 | - <li key={link.name}> | |
| 109 | - <a | |
| 110 | - href={link.url} | |
| 111 | - className={classnames(`${link.name}-link`, { 'active-link': isActiveLink(link) })} | |
| 112 | - {...link.external && { target: '_blank', rel: 'noopener noreferrer' }} | |
| 113 | - > | |
| 114 | - {link.label} | |
| 115 | - </a> | |
| 116 | - </li>)} | |
| 98 | + <UpperNavItems /> | |
| 99 | + </ul> | |
| 100 | + </details> | |
| 101 | + </li> | |
| 117 | 102 | |
| 118 | - {shouldShowUpsell() && ( | |
| 119 | - <li> | |
| 120 | - <a | |
| 121 | - className="button button-large button-secondary" | |
| 122 | - href="https://codesnippets.pro/pricing/" | |
| 123 | - target="_blank" rel="noopener noreferrer" | |
| 124 | - onClick={event => { | |
| 125 | - event.preventDefault() | |
| 126 | - setIsUpsellDialogOpen(true) | |
| 127 | - }} | |
| 128 | - > | |
| 129 | - {__('Upgrade to Pro', 'code-snippets')} | |
| 130 | - </a> | |
| 131 | - </li>)} | |
| 132 | - </ul> | |
| 133 | - </nav> | |
| 134 | - </div> | |
| 103 | +const UpperNav: React.FC = () => { | |
| 104 | + const [isUpsellDialogOpen, setIsUpsellDialogOpen] = useState(false) | |
| 135 | 105 | |
| 136 | -const currentPage = fetchQueryParam('page') | |
| 137 | -const currentSubpage = fetchQueryParam('subpage') | |
| 106 | + return ( | |
| 107 | + <div className="code-snippets-toolbar-upper"> | |
| 108 | + <div className="logo"> | |
| 109 | + <img | |
| 110 | + src={`${window.CODE_SNIPPETS?.urls.plugin}/assets/icon.svg`} | |
| 111 | + alt={__('Code Snippets logo', 'code-snippets')} | |
| 112 | + aria-hidden="true" | |
| 113 | + /> | |
| 138 | 114 | |
| 139 | -const isActiveLink = ({ pageSlug, subpage }: Readonly<NavLink>): boolean => { | |
| 140 | - if (subpage) { | |
| 141 | - return currentSubpage === subpage | |
| 142 | - } | |
| 143 | - if (pageSlug) { | |
| 144 | - return !currentSubpage && currentPage === pageSlug | |
| 145 | - } | |
| 146 | - return false | |
| 115 | + <div>{__('Code Snippets', 'code-snippets')}</div> | |
| 116 | + </div> | |
| 117 | + | |
| 118 | + <nav aria-label={__('Main links', 'code-snippets')}> | |
| 119 | + <ul> | |
| 120 | + <UpperNavItems /> | |
| 121 | + <MoreNav /> | |
| 122 | + | |
| 123 | + {shouldShowUpsell() && ( | |
| 124 | + <li className="toolbar-upgrade-item"> | |
| 125 | + <a | |
| 126 | + className="button button-large button-secondary" | |
| 127 | + href="https://codesnippets.pro/pricing/" | |
| 128 | + target="_blank" rel="noopener noreferrer" | |
| 129 | + onClick={event => { | |
| 130 | + event.preventDefault() | |
| 131 | + setIsUpsellDialogOpen(true) | |
| 132 | + }} | |
| 133 | + > | |
| 134 | + {__('Upgrade to Pro', 'code-snippets')} | |
| 135 | + </a> | |
| 136 | + </li>)} | |
| 137 | + </ul> | |
| 138 | + </nav> | |
| 139 | + | |
| 140 | + <UpsellDialog isOpen={isUpsellDialogOpen} setIsOpen={setIsUpsellDialogOpen} /> | |
| 141 | + </div> | |
| 142 | + ) | |
| 147 | 143 | } |
| 148 | 144 | |
| 145 | +interface SubpageItemProps { | |
| 146 | + subpage: SubpageName | |
| 147 | + label: string | |
| 148 | + Icon: React.FC<SVGProps<SVGSVGElement>> | |
| 149 | +} | |
| 150 | + | |
| 151 | +const SubpageItem: React.FC<SubpageItemProps> = ({ subpage, label, Icon }) => { | |
| 152 | + const { demo, isPro } = SUBPAGE_ENTRIES[subpage] | |
| 153 | + const isNew = 'announce' === demo && !hasSeenDemo(subpage) | |
| 154 | + | |
| 155 | + return ( | |
| 156 | + <li> | |
| 157 | + <a | |
| 158 | + href={buildUrl(window.CODE_SNIPPETS?.urls.manage, { subpage: subpage })} | |
| 159 | + className={classnames(`${subpage}-link`, { 'active-link': subpage === activeSubpage })} | |
| 160 | + > | |
| 161 | + <Icon aria-hidden="true" /> | |
| 162 | + <span className="toolbar-nav-label">{label}</span> | |
| 163 | + {demo && (isNew | |
| 164 | + ? <span className="new-chip">{__('New', 'code-snippets')}</span> | |
| 165 | + : <span className="demo-chip">{__('Demo', 'code-snippets')}</span>)} | |
| 166 | + {isPro && !isLicensed() && <span className="pro-chip">{__('Pro', 'code-snippets')}</span>} | |
| 167 | + </a> | |
| 168 | + </li> | |
| 169 | + ) | |
| 170 | +} | |
| 171 | + | |
| 172 | +const SettingsItem = () => | |
| 173 | + window.CODE_SNIPPETS?.urls.settings | |
| 174 | + ? <li> | |
| 175 | + <a | |
| 176 | + href={window.CODE_SNIPPETS.urls.settings} | |
| 177 | + className={classnames('settings-link', { | |
| 178 | + 'active-link': 'snippets-settings' === searchParams.get('page') | |
| 179 | + })} | |
| 180 | + > | |
| 181 | + <SettingsIcon aria-hidden="true" /> | |
| 182 | + <span className="toolbar-nav-label">{__('Settings', 'code-snippets')}</span> | |
| 183 | + </a> | |
| 184 | + </li> | |
| 185 | + : null | |
| 186 | + | |
| 149 | 187 | const LowerNav = () => |
| 150 | 188 | <div className="code-snippets-toolbar-lower"> |
| 151 | 189 | <nav aria-label={__('Main features', 'code-snippets')}> |
| 152 | 190 | <ul> |
| 153 | - {LOWER_NAV_LINKS.map(link => | |
| 154 | - <li key={link.name} className={link.end ? 'toolbar-end-item' : undefined}> | |
| 155 | - <a | |
| 156 | - href={link.url ?? buildUrl(window.CODE_SNIPPETS?.urls.manage, { subpage: link.name })} | |
| 157 | - className={classnames(`${link.name}-link`, { 'active-link': isActiveLink(link) })} | |
| 158 | - > | |
| 159 | - {link.icon} | |
| 160 | - <span className="toolbar-nav-label">{link.label}</span> | |
| 161 | - {link.pro && !isLicensed() && <span className="pro-chip">{__('Pro', 'code-snippets')}</span>} | |
| 162 | - </a> | |
| 163 | - </li>)} | |
| 191 | + <SubpageItem | |
| 192 | + subpage="snippets" | |
| 193 | + label={__('Snippets', 'code-snippets')} | |
| 194 | + Icon={SnippetsIcon} | |
| 195 | + /> | |
| 196 | + | |
| 197 | + {isLicensed() && ( | |
| 198 | + <SubpageItem | |
| 199 | + subpage="blueprints" | |
| 200 | + label={__('Blueprints', 'code-snippets')} | |
| 201 | + Icon={BlueprintIcon} | |
| 202 | + />)} | |
| 203 | + | |
| 204 | + <SubpageItem | |
| 205 | + subpage="cloud-community" | |
| 206 | + label={__('Cloud Community', 'code-snippets')} | |
| 207 | + Icon={CommunityIcon} | |
| 208 | + /> | |
| 209 | + | |
| 210 | + <SubpageItem | |
| 211 | + subpage="cloud-library" | |
| 212 | + label={__('Cloud Library', 'code-snippets')} | |
| 213 | + Icon={LibraryIcon} | |
| 214 | + /> | |
| 215 | + | |
| 216 | + {!isLicensed() && ( | |
| 217 | + <SubpageItem | |
| 218 | + subpage="blueprints" | |
| 219 | + label={__('Blueprints', 'code-snippets')} | |
| 220 | + Icon={BlueprintIcon} | |
| 221 | + />)} | |
| 222 | + | |
| 223 | + <SubpageItem | |
| 224 | + subpage="ai-agent" | |
| 225 | + label={__('AI Agent', 'code-snippets')} | |
| 226 | + Icon={AiAgentIcon} | |
| 227 | + /> | |
| 228 | + | |
| 229 | + <SettingsItem /> | |
| 164 | 230 | </ul> |
| 165 | 231 | </nav> |
| 166 | 232 | </div> |
| 167 | 233 | |
| 168 | -export const Toolbar = () => { | |
| 169 | - const [isUpsellDialogOpen, setIsUpsellDialogOpen] = useState(false) | |
| 170 | - | |
| 171 | - return ( | |
| 172 | - <div className="code-snippets-toolbar"> | |
| 173 | - <UpperNav setIsUpsellDialogOpen={setIsUpsellDialogOpen} /> | |
| 174 | - <LowerNav /> | |
| 175 | - <UpsellDialog isOpen={isUpsellDialogOpen} setIsOpen={setIsUpsellDialogOpen} /> | |
| 176 | - </div> | |
| 177 | - ) | |
| 178 | -} | |
| 234 | +export const Toolbar = () => | |
| 235 | + <div className="code-snippets-toolbar"> | |
| 236 | + <UpperNav /> | |
| 237 | + <LowerNav /> | |
| 238 | + </div> | |