Initial commit: WordPress wp-content (themes, plugins, languages)

- Theme: momentry (custom theme with REST API routes)
- Plugins: code-snippets (contains all API proxies)
- Languages: zh_TW translations
- Excludes: cache, backups, uploads, logs
This commit is contained in:
OpenCode
2026-05-29 19:07:56 +08:00
commit 09ef1f000f
6521 changed files with 867163 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
import React from 'react'
import classnames from 'classnames'
import { __ } from '@wordpress/i18n'
import { isLicensed } from '../../utils/screen'
import { isCondition } from '../../utils/snippets/snippets'
import { Badge } from '../common/Badge'
import { Button } from '../common/Button'
import { useSnippetForm } from '../../hooks/useSnippetForm'
import type { Dispatch, SetStateAction } from 'react'
export interface ConditionModalButtonProps {
setIsDialogOpen: Dispatch<SetStateAction<boolean>>
}
export const ConditionModalButton: React.FC<ConditionModalButtonProps> = ({ setIsDialogOpen }) => {
const { snippet, isReadOnly } = useSnippetForm()
const hasCondition = 0 !== snippet.conditionId
return (
<div className={classnames('conditions-editor-open block-form-field', hasCondition ? 'has-condition' : 'no-condition')}>
{isCondition(snippet) ? null
: <>
<h4>
{__('Conditions', 'code-snippets')}
<Badge name="beta" small>{__('beta', 'code-snippets')}</Badge>
{!isLicensed() && <Badge name="pro" small>{__('Pro', 'code-snippets')}</Badge>}
</h4>
<Button large disabled={isReadOnly} onClick={() => setIsDialogOpen(true)}>
<Badge name="cond" small />
{hasCondition
? __('Edit Conditions', 'code-snippets')
: __('Add Conditions', 'code-snippets')}
</Button>
</>}
</div>
)
}

View File

@@ -0,0 +1,56 @@
import React from 'react'
import { Spinner } from '@wordpress/components'
import { isRTL } from '@wordpress/i18n'
import { useSnippetForm } from '../../hooks/useSnippetForm'
import { isNetworkAdmin } from '../../utils/screen'
import { isCondition } from '../../utils/snippets/snippets'
import { ConditionModalButton } from '../ConditionModal/ConditionModalButton'
import { SnippetLocationInput } from '../SnippetForm/fields/SnippetLocationInput'
import { Notices } from '../SnippetForm/page/Notices'
import { ShortcodeInfo } from './actions/ShortcodeInfo'
import { MultisiteSharingSettings } from './controls/MultisiteSharingSettings'
import { ExportButtons } from './actions/ExportButtons'
import { SubmitButtons } from './actions/SubmitButtons'
import { ActivationSwitch } from './controls/ActivationSwitch'
import { DeleteButton } from './actions/DeleteButton'
import { PriorityInput } from './controls/PriorityInput'
import { RTLControl } from './controls/RTLControl'
import type { Dispatch, SetStateAction } from 'react'
export interface EditorSidebarProps {
setIsUpgradeDialogOpen: Dispatch<SetStateAction<boolean>>
}
export const EditorSidebar: React.FC<EditorSidebarProps> = ({ setIsUpgradeDialogOpen }) => {
const { snippet, isWorking } = useSnippetForm()
return (
<div className="snippet-editor-sidebar">
<div className="box">
{snippet.id && !isCondition(snippet) ? <ActivationSwitch /> : null}
{isNetworkAdmin() ? <MultisiteSharingSettings /> : null}
{isRTL() ? <RTLControl /> : null}
<ConditionModalButton setIsDialogOpen={setIsUpgradeDialogOpen} />
<SnippetLocationInput />
<ShortcodeInfo />
<PriorityInput />
{snippet.id
? <div className="row-actions visible inline-form-field">
<ExportButtons />
<DeleteButton />
</div> : null}
</div>
<p className="submit">
<SubmitButtons />
{isWorking ? <Spinner /> : ''}
</p>
<Notices />
</div>
)
}

View File

@@ -0,0 +1,52 @@
import { addQueryArgs } from '@wordpress/url'
import React, { useState } from 'react'
import { __ } from '@wordpress/i18n'
import { useRestAPI } from '../../../hooks/useRestAPI'
import { Button } from '../../common/Button'
import { ConfirmDialog } from '../../common/ConfirmDialog'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
export const DeleteButton: React.FC = () => {
const { snippetsAPI } = useRestAPI()
const { snippet, setIsWorking, isWorking, handleRequestError } = useSnippetForm()
const [isDialogOpen, setIsDialogOpen] = useState(false)
return (
<>
<Button
id="delete-snippet"
className="delete-button"
disabled={isWorking}
onClick={() => {
setIsDialogOpen(true)
}}
>
{__('Delete', 'code-snippets')}
</Button>
<ConfirmDialog
open={isDialogOpen}
title={__('Delete?', 'code-snippets')}
confirmLabel={__('Delete', 'code-snippets')}
confirmButtonClassName="is-destructive"
onCancel={() => setIsDialogOpen(false)}
onConfirm={() => {
setIsDialogOpen(false)
setIsWorking(true)
snippetsAPI.delete(snippet)
.then(() => {
setIsWorking(false)
window.location.replace(addQueryArgs(window.CODE_SNIPPETS?.urls.manage, { result: 'deleted' }))
})
.catch((error: unknown) => handleRequestError(error, __('Could not delete snippet.', 'code-snippets')))
}}
>
<p style={{ marginBlockStart: 0 }}>
{__('You are about to delete this snippet.', 'code-snippets')}{' '}
{__('Are you sure?', 'code-snippets')}
</p>
</ConfirmDialog>
</>
)
}

View File

@@ -0,0 +1,56 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { useRestAPI } from '../../../hooks/useRestAPI'
import { Button } from '../../common/Button'
import { downloadSnippetExportFile } from '../../../utils/files'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
import type { Snippet } from '../../../types/Snippet'
import type { SnippetsExport } from '../../../types/schema/SnippetsExport'
interface ExportButtonProps {
name: string
label: string
makeRequest: (snippet: Snippet) => Promise<SnippetsExport | string>
}
const ExportButton: React.FC<ExportButtonProps> = ({ name, label, makeRequest }) => {
const { snippet, isWorking, setIsWorking, handleRequestError } = useSnippetForm()
const handleClick = () => {
setIsWorking(true)
makeRequest(snippet)
.then(response => downloadSnippetExportFile(response, snippet))
// translators: %s: error message.
.catch((error: unknown) => handleRequestError(error, __('Could not download export file.', 'code-snippets')))
.finally(() => setIsWorking(false))
}
return (
<Button name={name} onClick={handleClick} disabled={isWorking}>
{label}
</Button>
)
}
export const ExportButtons: React.FC = () => {
const { snippetsAPI } = useRestAPI()
return (
<div className="snippet-export-buttons">
<ExportButton
name="export_snippet"
label={__('Export', 'code-snippets')}
makeRequest={snippetsAPI.export}
/>
{window.CODE_SNIPPETS_EDIT?.enableDownloads
? <ExportButton
name="export_snippet_code"
label={__('Export Code', 'code-snippets')}
makeRequest={snippetsAPI.exportCode}
/>
: null}
</div>
)
}

View File

@@ -0,0 +1,157 @@
import React, { useState } from 'react'
import { CheckboxControl, ExternalLink, Modal } from '@wordpress/components'
import { __ } from '@wordpress/i18n'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
import { Button } from '../../common/Button'
import { CopyToClipboardButton } from '../../common/CopyToClipboardButton'
import type { Dispatch, SetStateAction } from 'react'
type ShortcodeAtts = Record<string, unknown>
const buildShortcodeTag = (tag: string, atts: ShortcodeAtts): string =>
`[${[
tag,
...Object.entries(atts)
.filter(([, value]) => Boolean(value))
.map(([att, value]) =>
'boolean' === typeof value ? att : `${att}=${JSON.stringify(value)}`)
].filter(Boolean).join(' ')}]`
const SHORTCODE_TAG = 'code_snippet'
interface ShortcodeOptions {
php: boolean
format: boolean
shortcodes: boolean
}
interface CheckboxListProps<T extends string> {
options: T[]
checked: Record<T, boolean>
disabled: boolean
setChecked: Dispatch<SetStateAction<Record<T, boolean>>>
optionLabels: Partial<Record<T, string>>
optionDescriptions: Partial<Record<T, string>>
}
const CheckboxList = <T extends string>({
options,
checked,
disabled,
setChecked,
optionLabels,
optionDescriptions
}: CheckboxListProps<T>) =>
<ul>
{options.map(option =>
<li key={option}>
<CheckboxControl
label={optionLabels[option]}
help={optionDescriptions[option]}
checked={checked[option]}
disabled={disabled}
onChange={value =>
setChecked(previous => ({ ...previous, [option]: value }))}
/>
</li>)}
</ul>
const ShortcodeDescription = () =>
<p className="description">
{__('Copy the below shortcode to insert this snippet into a post, page, or other content.', 'code-snippets')}{'\n'}
{__('You can also use the Classic Editor button, Block editor (Pro) or Elementor widget (Pro).', 'code-snippets')}{'\n'}
<ExternalLink
href={__('https://codesnippets.pro/doc/inserting-content-snippets/', 'code-snippets')}
>
{__('Learn more', 'code-snippets')}
</ExternalLink>
</p>
const OPTION_LABELS: Record<keyof ShortcodeOptions, string> = {
php: __('Evaluate PHP code', 'code-snippets'),
format: __('Add paragraphs and formatting', 'code-snippets'),
shortcodes: __('Evaluate additional shortcode tags', 'code-snippets')
}
const OPTION_DESCRIPTIONS: Record<keyof ShortcodeOptions, string> = {
php: __('Run code within <?php ?> tags.', 'code-snippets'),
format: __('Wrap output in paragraphs and apply formatting.', 'code-snippets'),
shortcodes: __('Replace [shortcodes] embedded within the snippet.', 'code-snippets')
}
const ModalContent = () => {
const { snippet, isReadOnly } = useSnippetForm()
const [options, setOptions] = useState<ShortcodeOptions>(() => ({
php: snippet.code.includes('<?'),
format: true,
shortcodes: false
}))
const shortcodeAtts: ShortcodeAtts = {
id: snippet.id,
network: snippet.network,
...options,
name: snippet.name
}
const shortcodeTag = buildShortcodeTag(SHORTCODE_TAG, shortcodeAtts)
return (
<>
<ShortcodeDescription />
<p className="shortcode-tag-wrapper">
<code className="shortcode-tag">{shortcodeTag}</code>
<CopyToClipboardButton primary text={shortcodeTag} />
</p>
<p>
<h4>{__('Shortcode Options', 'code-snippets')}</h4>
<CheckboxList
options={['php', 'format', 'shortcodes']}
checked={options}
disabled={isReadOnly}
setChecked={setOptions}
optionLabels={OPTION_LABELS}
optionDescriptions={OPTION_DESCRIPTIONS}
/>
</p>
</>
)
}
export const ShortcodeInfo: React.FC = () => {
const { snippet, isReadOnly } = useSnippetForm()
const [isModalOpen, setIsModalOpen] = useState(false)
return 'content' === snippet.scope && snippet.id
? <div className="inline-form-field">
<h4>{__('Shortcode', 'code-snippets')}</h4>
<Button onClick={() => setIsModalOpen(true)} disabled={isReadOnly}>
{__('See options', 'code-snippets')}
</Button>
{isModalOpen
? <Modal
size="medium"
className="code-snippets-modal"
title={__('Embed Snippet with Shortcode', 'code-snippets')}
onRequestClose={() => setIsModalOpen(false)}
>
<div className="modal-content">
<ModalContent />
</div>
<div className="modal-footer">
<Button link large onClick={() => setIsModalOpen(false)}>
{__('Close Popup', 'code-snippets')}
</Button>
</div>
</Modal>
: null}
</div>
: null
}

View File

@@ -0,0 +1,83 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { SubmitSnippetAction } from '../../../hooks/useSubmitSnippet'
import { isCondition } from '../../../utils/snippets/snippets'
import { isNetworkAdmin } from '../../../utils/screen'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
import { SubmitButton } from '../../common/SubmitButton'
import type { SubmitButtonProps } from '../../common/SubmitButton'
const SaveButton = (props: SubmitButtonProps) => {
const { snippet } = useSnippetForm()
return (
<SubmitButton
large
name={SubmitSnippetAction.SAVE}
text={isCondition(snippet)
? __('Save Condition', 'code-snippets')
: __('Save Snippet', 'code-snippets')}
{...props}
/>
)
}
interface ActivateOrDeactivateButtonProps {
primaryActivate: boolean
}
const ActivateOrDeactivateButton: React.FC<ActivateOrDeactivateButtonProps> = ({ primaryActivate }) => {
const { snippet, isWorking } = useSnippetForm()
switch (true) {
case isCondition(snippet) || snippet.shared_network && isNetworkAdmin():
return null
case 'single-use' === snippet.scope:
return (
<SubmitButton
large
name={SubmitSnippetAction.SAVE_AND_EXECUTE}
disabled={isWorking}
text={__('Save and Execute Once', 'code-snippets')}
/>
)
case snippet.active:
return (
<SubmitButton
name={SubmitSnippetAction.SAVE_AND_DEACTIVATE}
disabled={isWorking}
large
text={__('Save and Deactivate', 'code-snippets')}
/>
)
default:
case !snippet.active:
return (
<SubmitButton
name={SubmitSnippetAction.SAVE_AND_ACTIVATE}
primary={primaryActivate}
disabled={isWorking}
large
text={__('Save and Activate', 'code-snippets')}
/>
)
}
}
export const SubmitButtons: React.FC = () => {
const { snippet } = useSnippetForm()
const activateByDefault =
!!window.CODE_SNIPPETS_EDIT?.activateByDefault &&
!snippet.active && 'single-use' !== snippet.scope &&
(!snippet.shared_network || !isNetworkAdmin())
return <>
{activateByDefault && <SaveButton primary={!activateByDefault} />}
<ActivateOrDeactivateButton primaryActivate={activateByDefault} />
{!activateByDefault && <SaveButton primary />}
</>
}

View File

@@ -0,0 +1,37 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
import { SubmitSnippetAction, useSubmitSnippet } from '../../../hooks/useSubmitSnippet'
import { handleUnknownError } from '../../../utils/errors'
export const ActivationSwitch = () => {
const { snippet, isWorking } = useSnippetForm()
const { submitSnippet } = useSubmitSnippet()
return (
<div className="inline-form-field activation-switch-container">
<h4>{__('Status')}</h4>
<label>
{snippet.active
? __('Active', 'code-snippets')
: __('Inactive', 'code-snippets')}
<input
id="activation-switch"
type="checkbox"
checked={snippet.active}
disabled={isWorking || !!snippet.shared_network}
className="switch"
onChange={() => {
submitSnippet(snippet.active
? SubmitSnippetAction.SAVE_AND_DEACTIVATE
: SubmitSnippetAction.SAVE_AND_ACTIVATE)
.then(() => undefined)
.catch(handleUnknownError)
}}
/>
</label>
</div>
)
}

View File

@@ -0,0 +1,41 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
import { Tooltip } from '../../common/Tooltip'
export const MultisiteSharingSettings: React.FC = () => {
const { snippet, setSnippet, isReadOnly } = useSnippetForm()
return (
<div className="inline-form-field activation-switch-container">
<h4>
{__('Share with Subsites', 'code-snippets')}
</h4>
<Tooltip inline start>
{__('Instead of running on every site, allow this snippet to be activated on individual sites on the network.', 'code-snippets')}
</Tooltip>
<label>
{snippet.shared_network
? __('Enabled', 'code-snippets')
: __('Disabled', 'code-snippets')}
<input
id="snippet_sharing"
name="snippet_sharing"
type="checkbox"
className="switch"
checked={!!snippet.shared_network}
disabled={isReadOnly}
onChange={event =>
setSnippet(previous => ({
...previous,
active: false,
shared_network: event.target.checked
}))}
/>
</label>
</div>
)
}

View File

@@ -0,0 +1,34 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
import { Tooltip } from '../../common/Tooltip'
export const PriorityInput = () => {
const { snippet, isReadOnly, setSnippet } = useSnippetForm()
return (
<div className="snippet-priority inline-form-field">
<h4>
<label htmlFor="snippet-priority">
{__('Priority', 'code-snippets')}
</label>
</h4>
<Tooltip block end>
{__('Snippets with a lower priority number will run before those with a higher number.', 'code-snippets')}
</Tooltip>
<input
type="number"
id="snippet-priority"
name="snippet_priority"
value={snippet.priority}
disabled={isReadOnly}
onChange={event => setSnippet(previous => ({
...previous,
priority: parseInt(event.target.value, 10)
}))}
/>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
export const RTLControl: React.FC = () => {
const { codeEditorInstance } = useSnippetForm()
return (
<div className="inline-form-field">
<h4>
<label htmlFor="snippet-code-direction">
{__('Code Direction', 'code-snippets')}
</label>
</h4>
<select id="snippet-code-direction" onChange={event =>
codeEditorInstance?.codemirror.setOption('direction', 'rtl' === event.target.value ? 'rtl' : 'ltr')
}>
<option value="ltr">{__('LTR', 'code-snippets')}</option>
<option value="rtl">{__('RTL', 'code-snippets')}</option>
</select>
</div>
)
}

View File

@@ -0,0 +1 @@
export * from './EditorSidebar'

View File

@@ -0,0 +1,201 @@
import React, { useState, useRef, useEffect } from 'react'
import { __ } from '@wordpress/i18n'
import { Button } from '../../common/Button'
import {
DuplicateActionSelector,
DragDropUploadArea,
SelectedFilesList,
SnippetSelectionTable,
ImportResultDisplay
} from './components'
import { ImportCard } from '../shared'
import {
useFileSelection,
useSnippetSelection,
useImportWorkflow
} from './hooks'
type DuplicateAction = 'ignore' | 'replace' | 'skip'
type Step = 'upload' | 'select'
export const FileUploadForm: React.FC = () => {
const [duplicateAction, setDuplicateAction] = useState<DuplicateAction>('ignore')
const [currentStep, setCurrentStep] = useState<Step>('upload')
const selectSectionRef = useRef<HTMLDivElement>(null)
const fileSelection = useFileSelection()
const importWorkflow = useImportWorkflow()
const snippetSelection = useSnippetSelection(importWorkflow.availableSnippets)
useEffect(() => {
if (currentStep === 'select' && selectSectionRef.current) {
selectSectionRef.current.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
}
}, [currentStep])
const handleFileSelect = (files: FileList | null) => {
fileSelection.handleFileSelect(files)
importWorkflow.clearUploadResult()
}
const handleParseFiles = async () => {
if (!fileSelection.selectedFiles) return
const success = await importWorkflow.parseFiles(fileSelection.selectedFiles)
if (success) {
snippetSelection.clearSelection()
setCurrentStep('select')
}
}
const handleImportSelected = async () => {
const snippetsToImport = snippetSelection.getSelectedSnippets()
await importWorkflow.importSnippets(snippetsToImport, duplicateAction)
}
const handleBackToUpload = () => {
setCurrentStep('upload')
fileSelection.clearFiles()
snippetSelection.clearSelection()
importWorkflow.resetWorkflow()
}
const isUploadDisabled = !fileSelection.selectedFiles ||
fileSelection.selectedFiles.length === 0 ||
importWorkflow.isUploading
const isImportDisabled = snippetSelection.selectedSnippets.size === 0 ||
importWorkflow.isImporting
return (
<div className="wrap">
<div className="import-form-container" style={{ maxWidth: '800px' }}>
<p>{__('Upload one or more Code Snippets export files and the snippets will be imported.', 'code-snippets')}</p>
<p>
{__('Afterward, you will need to visit the ', 'code-snippets')}
<a href="admin.php?page=snippets">
{__('All Snippets', 'code-snippets')}
</a>
{__(' page to activate the imported snippets.', 'code-snippets')}
</p>
{currentStep === 'upload' && (
<>
{(!importWorkflow.uploadResult || !importWorkflow.uploadResult.success) && (
<>
<DuplicateActionSelector
value={duplicateAction}
onChange={setDuplicateAction}
/>
<ImportCard>
<h2 style={{ margin: '0 0 1em 0' }}>{__('Choose Files', 'code-snippets')}</h2>
<p className="description" style={{ marginBottom: '1em' }}>
{__('Choose one or more Code Snippets (.xml or .json) files to parse and preview.', 'code-snippets')}
</p>
<DragDropUploadArea
fileInputRef={fileSelection.fileInputRef}
onFileSelect={handleFileSelect}
disabled={importWorkflow.isUploading}
/>
{fileSelection.selectedFiles && fileSelection.selectedFiles.length > 0 && (
<SelectedFilesList
files={fileSelection.selectedFiles}
onRemoveFile={fileSelection.removeFile}
/>
)}
<div style={{ textAlign: 'center' }}>
<Button
primary
onClick={handleParseFiles}
disabled={isUploadDisabled}
style={{ minWidth: '200px' }}
>
{importWorkflow.isUploading
? __('Uploading files...', 'code-snippets')
: __('Upload files', 'code-snippets')
}
</Button>
</div>
</ImportCard>
</>
)}
</>
)}
{currentStep === 'select' && importWorkflow.availableSnippets.length > 0 && !importWorkflow.uploadResult?.success && (
<ImportCard ref={selectSectionRef}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '20px' }}>
<Button onClick={handleBackToUpload} className="button-link">
{__('← Upload Different Files', 'code-snippets')}
</Button>
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<div>
<h3 style={{ margin: '0' }}>{__('Available Snippets', 'code-snippets')} ({importWorkflow.availableSnippets.length})</h3>
<p style={{ margin: '0.5em 0 1em 0', color: '#666' }}>
{__('Select the snippets you want to import:', 'code-snippets')}
</p>
</div>
<div>
<Button onClick={snippetSelection.handleSelectAll} style={{ marginRight: '10px' }}>
{snippetSelection.isAllSelected
? __('Deselect All', 'code-snippets')
: __('Select All', 'code-snippets')
}
</Button>
<Button
primary
onClick={handleImportSelected}
disabled={isImportDisabled}
>
{importWorkflow.isImporting
? __('Importing...', 'code-snippets')
: __('Import Selected', 'code-snippets')} ({snippetSelection.selectedSnippets.size})
</Button>
</div>
</div>
<SnippetSelectionTable
snippets={importWorkflow.availableSnippets}
selectedSnippets={snippetSelection.selectedSnippets}
isAllSelected={snippetSelection.isAllSelected}
onSnippetToggle={snippetSelection.handleSnippetToggle}
onSelectAll={snippetSelection.handleSelectAll}
/>
<div style={{ textAlign: 'end', marginTop: '1em' }}>
<Button onClick={snippetSelection.handleSelectAll} style={{ marginRight: '10px' }}>
{snippetSelection.isAllSelected
? __('Deselect All', 'code-snippets')
: __('Select All', 'code-snippets')
}
</Button>
<Button
primary
onClick={handleImportSelected}
disabled={isImportDisabled}
>
{importWorkflow.isImporting
? __('Importing...', 'code-snippets')
: __('Import Selected', 'code-snippets')} ({snippetSelection.selectedSnippets.size})
</Button>
</div>
</ImportCard>
)}
{importWorkflow.uploadResult && (
<ImportResultDisplay result={importWorkflow.uploadResult} />
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,66 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { useDragAndDrop } from '../hooks/useDragAndDrop'
interface DragDropUploadAreaProps {
fileInputRef: React.RefObject<HTMLInputElement>
onFileSelect: (files: FileList | null) => void
disabled?: boolean
}
export const DragDropUploadArea: React.FC<DragDropUploadAreaProps> = ({
fileInputRef,
onFileSelect,
disabled = false
}) => {
const { dragOver, handleDragOver, handleDragLeave, handleDrop } = useDragAndDrop({
onFilesDrop: onFileSelect
})
const handleClick = () => {
if (!disabled) {
fileInputRef.current?.click()
}
}
return (
<>
<div
className={`upload-drop-zone ${dragOver ? 'drag-over' : ''}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={handleClick}
style={{
border: `2px dashed ${dragOver ? '#0073aa' : '#ccd0d4'}`,
borderRadius: '4px',
padding: '40px 20px',
textAlign: 'center',
cursor: disabled ? 'not-allowed' : 'pointer',
backgroundColor: dragOver ? '#f0f6fc' : disabled ? '#f6f7f7' : '#fafafa',
marginBottom: '20px',
transition: 'all 0.3s ease',
opacity: disabled ? 0.6 : 1
}}
>
<div style={{ fontSize: '48px', marginBottom: '10px', color: '#666' }}>📁</div>
<p style={{ margin: '0 0 8px 0', fontSize: '16px', fontWeight: '500' }}>
{__('Drag and drop files here, or click to browse', 'code-snippets')}
</p>
<p style={{ margin: '0', color: '#666', fontSize: '14px' }}>
{__('Supports JSON and XML files', 'code-snippets')}
</p>
</div>
<input
ref={fileInputRef}
type="file"
accept="application/json,.json,text/xml"
multiple
onChange={(e) => onFileSelect(e.target.files)}
style={{ display: 'none' }}
disabled={disabled}
/>
</>
)
}

View File

@@ -0,0 +1,70 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { ImportCard } from '../../shared'
type DuplicateAction = 'ignore' | 'replace' | 'skip'
interface DuplicateActionSelectorProps {
value: DuplicateAction
onChange: (action: DuplicateAction) => void
}
export const DuplicateActionSelector: React.FC<DuplicateActionSelectorProps> = ({
value,
onChange
}) => {
return (
<ImportCard>
<h2 style={{ margin: '0 0 1em 0' }}>{__('Duplicate Snippets', 'code-snippets')}</h2>
<p className="description" style={{ marginBottom: '1em' }}>
{__('What should happen if an existing snippet is found with an identical name to an imported snippet?', 'code-snippets')}
</p>
<fieldset>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', cursor: 'pointer' }}>
<input
type="radio"
name="duplicate_action"
value="ignore"
checked={value === 'ignore'}
onChange={(e) => onChange(e.target.value as DuplicateAction)}
style={{ marginTop: '2px' }}
/>
<span>
{__('Ignore any duplicate snippets: import all snippets from the file regardless and leave all existing snippets unchanged.', 'code-snippets')}
</span>
</label>
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', cursor: 'pointer' }}>
<input
type="radio"
name="duplicate_action"
value="replace"
checked={value === 'replace'}
onChange={(e) => onChange(e.target.value as DuplicateAction)}
style={{ marginTop: '2px' }}
/>
<span>
{__('Replace any existing snippets with a newly imported snippet of the same name.', 'code-snippets')}
</span>
</label>
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', cursor: 'pointer' }}>
<input
type="radio"
name="duplicate_action"
value="skip"
checked={value === 'skip'}
onChange={(e) => onChange(e.target.value as DuplicateAction)}
style={{ marginTop: '2px' }}
/>
<span>
{__('Do not import any duplicate snippets; leave all existing snippets unchanged.', 'code-snippets')}
</span>
</label>
</div>
</fieldset>
</ImportCard>
)
}

View File

@@ -0,0 +1,74 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { ImportCard } from '../../shared'
interface ImportResult {
success: boolean
message: string
imported?: number
warnings?: string[]
}
interface ImportResultDisplayProps {
result: ImportResult
}
export const ImportResultDisplay: React.FC<ImportResultDisplayProps> = ({ result }) => {
return (
<ImportCard>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '12px' }}>
<div style={{
backgroundColor: result.success ? '#00a32a' : '#d63638',
borderRadius: '50%',
width: '24px',
height: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
marginTop: '2px'
}}>
<span style={{ color: 'white', fontSize: '14px', fontWeight: 'bold' }}>
{result.success ? '✓' : '✕'}
</span>
</div>
<div style={{ flex: 1 }}>
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px', fontWeight: '600' }}>
{result.success
? __('Import Successful!', 'code-snippets')
: __('Import Failed', 'code-snippets')
}
</h3>
<p style={{ margin: '0 0 8px 0', color: '#666' }}>
{result.message}
</p>
{result.success && (
<p style={{ margin: '0', color: '#666' }}>
{__('Go to ', 'code-snippets')}
<a href="admin.php?page=snippets" style={{ color: '#2271b1', textDecoration: 'none' }}>
{__('All Snippets', 'code-snippets')}
</a>
{__(' to activate your imported snippets.', 'code-snippets')}
</p>
)}
{result.warnings && result.warnings.length > 0 && (
<div style={{ marginTop: '12px' }}>
<h4 style={{ margin: '0 0 8px 0', fontSize: '14px', color: '#d63638' }}>
{__('Warnings:', 'code-snippets')}
</h4>
<ul style={{ margin: '0', paddingLeft: '20px' }}>
{result.warnings.map((warning, index) => (
<li key={index} style={{ color: '#666', fontSize: '14px' }}>
{warning}
</li>
))}
</ul>
</div>
)}
</div>
</div>
</ImportCard>
)
}

View File

@@ -0,0 +1,65 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { formatFileSize } from '../utils/fileUtils'
interface SelectedFilesListProps {
files: FileList
onRemoveFile: (index: number) => void
}
export const SelectedFilesList: React.FC<SelectedFilesListProps> = ({
files,
onRemoveFile
}) => {
return (
<div className="selected-files" style={{ marginBottom: '20px' }}>
<h3 style={{ margin: '0 0 12px 0', fontSize: '14px', fontWeight: '600' }}>
{__('Selected Files:', 'code-snippets')} ({files.length})
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{Array.from(files).map((file, index) => (
<div
key={index}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '8px 12px',
backgroundColor: '#f9f9f9',
borderRadius: '4px',
border: '1px solid #ddd'
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ fontSize: '16px' }}>📄</span>
<div>
<div style={{ fontWeight: '500' }}>{file.name}</div>
<div style={{ fontSize: '12px', color: '#666' }}>
{formatFileSize(file.size)}
</div>
</div>
</div>
<button
type="button"
onClick={(e) => {
e.stopPropagation()
onRemoveFile(index)
}}
style={{
background: 'none',
border: 'none',
color: '#d63638',
cursor: 'pointer',
fontSize: '16px',
padding: '4px'
}}
title={__('Remove file', 'code-snippets')}
>
</button>
</div>
))}
</div>
</div>
)
}

View File

@@ -0,0 +1,90 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import type { ImportableSnippet } from '../../../../hooks/useFileUploadAPI'
interface SnippetSelectionTableProps {
snippets: ImportableSnippet[]
selectedSnippets: Set<number | string>
isAllSelected: boolean
onSnippetToggle: (snippetId: number | string) => void
onSelectAll: () => void
}
export const SnippetSelectionTable: React.FC<SnippetSelectionTableProps> = ({
snippets,
selectedSnippets,
isAllSelected,
onSnippetToggle,
onSelectAll
}) => {
const getTypeColor = (type: string): string => {
switch (type) {
case 'css': return '#9B59B6'
case 'js': return '#FFEB3B'
case 'html': return '#EF6A36'
default: return '#1D97C6'
}
}
const truncateDescription = (description: string | undefined): string => {
const desc = description || __('No description', 'code-snippets')
return desc.length > 50 ? desc.substring(0, 50) + '...' : desc
}
return (
<table className="wp-list-table widefat fixed striped" style={{ borderRadius: '5px', tableLayout: 'fixed' }}>
<thead>
<tr>
<th scope="col" className="check-column" style={{ padding: '8px 0', width: '40px' }}>
<input
type="checkbox"
checked={isAllSelected}
onChange={onSelectAll}
/>
</th>
<th scope="col" style={{ width: '200px' }}>{__('Name', 'code-snippets')}</th>
<th scope="col" style={{ width: '90px', textAlign: 'center' }}>{__('Type', 'code-snippets')}</th>
<th scope="col" style={{ width: 'auto' }}>{__('Description', 'code-snippets')}</th>
<th scope="col" style={{ width: '120px' }}>{__('Tags', 'code-snippets')}</th>
</tr>
</thead>
<tbody>
{snippets.map(snippet => (
<tr key={snippet.table_data.id}>
<th scope="row" className="check-column">
<input
type="checkbox"
checked={selectedSnippets.has(snippet.table_data.id)}
onChange={() => onSnippetToggle(snippet.table_data.id)}
/>
</th>
<td>
<strong>{snippet.table_data.title}</strong>
{snippet.source_file && (
<div style={{ fontSize: '12px', color: '#666', marginTop: '2px' }}>
from {snippet.source_file}
</div>
)}
</td>
<td style={{ width: '90px', textAlign: 'center' }}>
<span style={{
backgroundColor: getTypeColor(snippet.table_data.type),
color: 'white',
padding: '3px 6px',
fontSize: '10px',
textTransform: 'uppercase',
borderRadius: '3px'
}}>
{snippet.table_data.type}
</span>
</td>
<td>
{truncateDescription(snippet.table_data.description)}
</td>
<td>{snippet.table_data.tags || '—'}</td>
</tr>
))}
</tbody>
</table>
)
}

View File

@@ -0,0 +1,5 @@
export { DuplicateActionSelector } from './DuplicateActionSelector'
export { DragDropUploadArea } from './DragDropUploadArea'
export { SelectedFilesList } from './SelectedFilesList'
export { SnippetSelectionTable } from './SnippetSelectionTable'
export { ImportResultDisplay } from './ImportResultDisplay'

View File

@@ -0,0 +1,4 @@
export { useDragAndDrop } from './useDragAndDrop'
export { useFileSelection } from './useFileSelection'
export { useSnippetSelection } from './useSnippetSelection'
export { useImportWorkflow } from './useImportWorkflow'

View File

@@ -0,0 +1,36 @@
import { useState } from 'react'
interface UseDragAndDropProps {
onFilesDrop: (files: FileList) => void
}
export const useDragAndDrop = ({ onFilesDrop }: UseDragAndDropProps) => {
const [dragOver, setDragOver] = useState(false)
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
setDragOver(true)
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
setDragOver(false)
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
setDragOver(false)
const files = e.dataTransfer.files
if (files.length > 0) {
onFilesDrop(files)
}
}
return {
dragOver,
handleDragOver,
handleDragLeave,
handleDrop
}
}

View File

@@ -0,0 +1,42 @@
import { useState, useRef } from 'react'
import { removeFileFromList } from '../utils/fileUtils'
export const useFileSelection = () => {
const [selectedFiles, setSelectedFiles] = useState<FileList | null>(null)
const fileInputRef = useRef<HTMLInputElement>(null)
const handleFileSelect = (files: FileList | null) => {
setSelectedFiles(files)
}
const removeFile = (index: number) => {
if (!selectedFiles) return
const newFiles = removeFileFromList(selectedFiles, index)
setSelectedFiles(newFiles)
if (fileInputRef.current) {
fileInputRef.current.files = newFiles
}
}
const clearFiles = () => {
setSelectedFiles(null)
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
}
const triggerFileInput = () => {
fileInputRef.current?.click()
}
return {
selectedFiles,
fileInputRef,
handleFileSelect,
removeFile,
clearFiles,
triggerFileInput
}
}

View File

@@ -0,0 +1,117 @@
import { useState } from 'react'
import { __ } from '@wordpress/i18n'
import { useFileUploadAPI, type ImportableSnippet } from '../../../../hooks/useFileUploadAPI'
import { isNetworkAdmin } from '../../../../utils/screen'
type DuplicateAction = 'ignore' | 'replace' | 'skip'
interface UploadResult {
success: boolean
message: string
imported?: number
warnings?: string[]
}
export const useImportWorkflow = () => {
const [isUploading, setIsUploading] = useState(false)
const [isImporting, setIsImporting] = useState(false)
const [availableSnippets, setAvailableSnippets] = useState<ImportableSnippet[]>([])
const [uploadResult, setUploadResult] = useState<UploadResult | null>(null)
const fileUploadAPI = useFileUploadAPI()
const parseFiles = async (files: FileList): Promise<boolean> => {
if (!files || files.length === 0) {
alert(__('Please select files to upload.', 'code-snippets'))
return false
}
setIsUploading(true)
setUploadResult(null)
try {
const response = await fileUploadAPI.parseFiles({ files })
setAvailableSnippets(response.data.snippets)
if (response.data.warnings && response.data.warnings.length > 0) {
setUploadResult({
success: true,
message: response.data.message,
warnings: response.data.warnings
})
}
return true
} catch (error) {
console.error('Parse error:', error)
setUploadResult({
success: false,
message: error instanceof Error ? error.message : __('An unknown error occurred.', 'code-snippets')
})
return false
} finally {
setIsUploading(false)
}
}
const importSnippets = async (
snippetsToImport: ImportableSnippet[],
duplicateAction: DuplicateAction
): Promise<boolean> => {
if (snippetsToImport.length === 0) {
alert(__('Please select snippets to import.', 'code-snippets'))
return false
}
setIsImporting(true)
setUploadResult(null)
try {
const response = await fileUploadAPI.importSnippets({
snippets: snippetsToImport,
duplicate_action: duplicateAction,
network: isNetworkAdmin()
})
setUploadResult({
success: true,
message: response.data.message,
imported: response.data.imported
})
return true
} catch (error) {
console.error('Import error:', error)
setUploadResult({
success: false,
message: error instanceof Error ? error.message : __('An unknown error occurred.', 'code-snippets')
})
return false
} finally {
setIsImporting(false)
}
}
const resetWorkflow = () => {
setAvailableSnippets([])
setUploadResult(null)
}
const clearUploadResult = () => {
setUploadResult(null)
}
return {
isUploading,
isImporting,
availableSnippets,
uploadResult,
parseFiles,
importSnippets,
resetWorkflow,
clearUploadResult
}
}

View File

@@ -0,0 +1,45 @@
import { useState } from 'react'
import type { ImportableSnippet } from '../../../../hooks/useFileUploadAPI'
export const useSnippetSelection = (availableSnippets: ImportableSnippet[]) => {
const [selectedSnippets, setSelectedSnippets] = useState<Set<number | string>>(new Set())
const handleSnippetToggle = (snippetId: number | string) => {
const newSelected = new Set(selectedSnippets)
if (newSelected.has(snippetId)) {
newSelected.delete(snippetId)
} else {
newSelected.add(snippetId)
}
setSelectedSnippets(newSelected)
}
const handleSelectAll = () => {
if (selectedSnippets.size === availableSnippets.length) {
setSelectedSnippets(new Set())
} else {
setSelectedSnippets(new Set(availableSnippets.map(snippet => snippet.table_data.id)))
}
}
const clearSelection = () => {
setSelectedSnippets(new Set())
}
const getSelectedSnippets = () => {
return availableSnippets.filter(snippet =>
selectedSnippets.has(snippet.table_data.id)
)
}
const isAllSelected = selectedSnippets.size === availableSnippets.length && availableSnippets.length > 0
return {
selectedSnippets,
handleSnippetToggle,
handleSelectAll,
clearSelection,
getSelectedSnippets,
isAllSelected
}
}

View File

@@ -0,0 +1,17 @@
export const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 Bytes'
const k = 1024
const sizes = ['Bytes', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
export const removeFileFromList = (fileList: FileList, indexToRemove: number): FileList => {
const dt = new DataTransfer()
for (let i = 0; i < fileList.length; i++) {
if (i !== indexToRemove) {
dt.items.add(fileList[i])
}
}
return dt.files
}

View File

@@ -0,0 +1,142 @@
import React, { useState } from 'react'
import { __ } from '@wordpress/i18n'
import {
ImporterSelector,
ImportOptions,
SimpleSnippetTable,
StatusDisplay
} from './components'
import { ImportCard } from '../shared'
import {
useImporterSelection,
useSnippetImport,
useImportSnippetSelection
} from './hooks'
export const ImportForm: React.FC = () => {
const [autoAddTags, setAutoAddTags] = useState<boolean>(false)
const importerSelection = useImporterSelection()
const snippetImport = useSnippetImport()
const snippetSelection = useImportSnippetSelection(snippetImport.snippets)
const handleImporterChange = async (newImporter: string) => {
importerSelection.handleImporterChange(newImporter)
snippetSelection.clearSelection()
snippetImport.resetAll()
if (newImporter) {
await snippetImport.loadSnippets(newImporter)
}
}
const handleImport = async () => {
const selectedIds = Array.from(snippetSelection.selectedSnippets)
const success = await snippetImport.importSnippets(
importerSelection.selectedImporter,
selectedIds,
autoAddTags,
importerSelection.tagValue
)
if (success) {
snippetSelection.clearSelection()
}
}
if (importerSelection.isLoading) {
return (
<div className="wrap">
<p>{__('Loading importers...', 'code-snippets')}</p>
</div>
)
}
if (importerSelection.error) {
return (
<div className="wrap">
<div className="notice notice-error">
<p>{__('Error loading importers:', 'code-snippets')} {importerSelection.error}</p>
</div>
</div>
)
}
return (
<div className="wrap">
<div className="import-form-container" style={{ maxWidth: '800px' }}>
<p>{__('If you are using another Snippets plugin, you can import all existing snippets to your Code Snippets library.', 'code-snippets')}</p>
<ImporterSelector
importers={importerSelection.importers}
selectedImporter={importerSelection.selectedImporter}
onImporterChange={handleImporterChange}
isLoading={snippetImport.isLoadingSnippets}
/>
{snippetImport.snippetsError && (
<StatusDisplay
type="error"
title={__('Error loading snippets', 'code-snippets')}
message={snippetImport.snippetsError}
/>
)}
{snippetImport.importError && (
<StatusDisplay
type="error"
title={__('Error importing snippets', 'code-snippets')}
message={snippetImport.importError}
/>
)}
{snippetImport.importSuccess.length > 0 && (
<StatusDisplay
type="success"
title={`${snippetImport.importSuccess.length} ${__('Snippets imported!', 'code-snippets')}`}
message={__('We successfully imported all snippets to your library. Go to ', 'code-snippets')}
showSnippetsLink
/>
)}
{importerSelection.selectedImporter &&
!snippetImport.isLoadingSnippets &&
!snippetImport.snippetsError &&
snippetImport.snippets.length === 0 &&
snippetImport.importSuccess.length === 0 && (
<ImportCard>
<div style={{ textAlign: 'center', padding: '40px 20px', color: '#666' }}>
<div style={{ fontSize: '48px', marginBottom: '16px' }}>📭</div>
<h3 style={{ margin: '0 0 8px 0', fontSize: '18px', color: '#333' }}>
{__('No snippets found', 'code-snippets')}
</h3>
<p style={{ margin: '0', fontSize: '14px' }}>
{__('No snippets were found for the selected plugin. Make sure the plugin is installed and has snippets configured.', 'code-snippets')}
</p>
</div>
</ImportCard>
)}
{snippetImport.snippets.length > 0 && (
<>
<ImportOptions
autoAddTags={autoAddTags}
tagValue={importerSelection.tagValue}
onAutoAddTagsChange={setAutoAddTags}
onTagValueChange={importerSelection.setTagValue}
/>
<SimpleSnippetTable
snippets={snippetImport.snippets}
selectedSnippets={snippetSelection.selectedSnippets}
onSnippetToggle={snippetSelection.handleSnippetToggle}
onSelectAll={snippetSelection.handleSelectAll}
onImport={handleImport}
isImporting={snippetImport.isImporting}
/>
</>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,52 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { ImportCard } from '../../shared'
interface ImportOptionsProps {
autoAddTags: boolean
tagValue: string
onAutoAddTagsChange: (enabled: boolean) => void
onTagValueChange: (value: string) => void
}
export const ImportOptions: React.FC<ImportOptionsProps> = ({
autoAddTags,
tagValue,
onAutoAddTagsChange,
onTagValueChange
}) => {
return (
<ImportCard>
<h2 style={{ margin: '0 0 1em 0' }}>{__('Import options', 'code-snippets')}</h2>
<label style={{ display: 'flex', alignItems: 'flex-start', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={autoAddTags}
onChange={(e) => onAutoAddTagsChange(e.target.checked)}
style={{ marginTop: '2px' }}
/>
<div style={{ flex: 1 }}>
<div>
<strong>{__('Automatically add Tag', 'code-snippets')}</strong>
<br />
<span style={{ color: '#666', fontSize: '0.9em' }}>
{__('For your convenience, we can add a tag on every imported snippet.', 'code-snippets')}
</span>
</div>
{autoAddTags && (
<div style={{ marginTop: '12px' }}>
<input
type="text"
value={tagValue}
onChange={(e) => onTagValueChange(e.target.value)}
placeholder={__('Add tag...', 'code-snippets')}
className="regular-text"
style={{ width: '100%', maxWidth: '300px' }}
/>
</div>
)}
</div>
</label>
</ImportCard>
)
}

View File

@@ -0,0 +1,50 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import type { Importer } from '../../../../hooks/useImportersAPI'
import { ImportCard } from '../../shared'
interface ImporterSelectorProps {
importers: Importer[]
selectedImporter: string
onImporterChange: (importerName: string) => void
isLoading: boolean
}
export const ImporterSelector: React.FC<ImporterSelectorProps> = ({
importers,
selectedImporter,
onImporterChange,
isLoading
}) => {
return (
<ImportCard variant="controls">
<label htmlFor="importer-select">
<h2 style={{ margin: '0 0 1em 0' }}>{__('Select Plugin', 'code-snippets')}</h2>
</label>
<select
id="importer-select"
value={selectedImporter}
onChange={(event) => onImporterChange(event.target.value)}
className="regular-text"
style={{ display: 'block', marginTop: '5px', width: '100%', maxWidth: '300px' }}
disabled={isLoading}
>
<option value="">{__('-- Select an importer --', 'code-snippets')}</option>
{importers.map(importer => (
<option
key={importer.name}
value={importer.name}
disabled={!importer.is_active}
>
{importer.title} {!importer.is_active ? __('(Inactive)', 'code-snippets') : ''}
</option>
))}
</select>
{isLoading && (
<p style={{ margin: '10px 0 0 0', color: '#666', fontSize: '14px' }}>
{__('Loading snippets...', 'code-snippets')}
</p>
)}
</ImportCard>
)
}

View File

@@ -0,0 +1,102 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { Button } from '../../../common/Button'
import type { ImportableSnippet } from '../../../../hooks/useImportersAPI'
import { ImportCard } from '../../shared'
interface SimpleSnippetTableProps {
snippets: ImportableSnippet[]
selectedSnippets: Set<number>
onSnippetToggle: (snippetId: number) => void
onSelectAll: () => void
onImport: () => void
isImporting: boolean
}
export const SimpleSnippetTable: React.FC<SimpleSnippetTableProps> = ({
snippets,
selectedSnippets,
onSnippetToggle,
onSelectAll,
onImport,
isImporting
}) => {
const isAllSelected = selectedSnippets.size === snippets.length && snippets.length > 0
return (
<ImportCard className="snippets-table-container">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '10px' }}>
<div>
<h2 style={{ margin: '0' }}>{__('Available Snippets', 'code-snippets')} ({snippets.length})</h2>
<p style={{ margin: '0.5em 0 1em 0' }}>{__('We found the following snippets.', 'code-snippets')}</p>
</div>
<div>
<Button onClick={onSelectAll} style={{ marginRight: '10px' }}>
{isAllSelected
? __('Deselect All', 'code-snippets')
: __('Select All', 'code-snippets')
}
</Button>
<Button
primary
onClick={onImport}
disabled={selectedSnippets.size === 0 || isImporting}
>
{isImporting
? __('Importing...', 'code-snippets')
: __('Import Selected', 'code-snippets')} ({selectedSnippets.size})
</Button>
</div>
</div>
<table className="wp-list-table widefat fixed striped" style={{ borderRadius: '5px' }}>
<thead>
<tr>
<th scope="col" className="check-column" style={{ padding: '8px 0' }}>
<input
type="checkbox"
checked={isAllSelected}
onChange={onSelectAll}
/>
</th>
<th scope="col">{__('Snippet Name', 'code-snippets')}</th>
<th scope="col" style={{ textAlign: 'end', width: '50px' }}>{__('ID', 'code-snippets')}</th>
</tr>
</thead>
<tbody>
{snippets.map(snippet => (
<tr key={snippet.table_data.id}>
<th scope="row" className="check-column">
<input
type="checkbox"
checked={selectedSnippets.has(snippet.table_data.id)}
onChange={() => onSnippetToggle(snippet.table_data.id)}
/>
</th>
<td>{snippet.table_data.title}</td>
<td style={{ textAlign: 'end', width: '50px' }}>{snippet.table_data.id}</td>
</tr>
))}
</tbody>
</table>
<div style={{ textAlign: 'end', marginTop: '1em' }}>
<Button onClick={onSelectAll} style={{ marginRight: '10px' }}>
{isAllSelected
? __('Deselect All', 'code-snippets')
: __('Select All', 'code-snippets')
}
</Button>
<Button
primary
onClick={onImport}
disabled={selectedSnippets.size === 0 || isImporting}
>
{isImporting
? __('Importing...', 'code-snippets')
: __('Import Selected', 'code-snippets')} ({selectedSnippets.size})
</Button>
</div>
</ImportCard>
)
}

View File

@@ -0,0 +1,55 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { ImportCard } from '../../shared'
interface StatusDisplayProps {
type: 'error' | 'success'
title: string
message: string
showSnippetsLink?: boolean
}
export const StatusDisplay: React.FC<StatusDisplayProps> = ({
type,
title,
message,
showSnippetsLink = false
}) => {
const isError = type === 'error'
return (
<ImportCard variant="controls" style={{ display: 'flex', alignItems: 'flex-start', gap: '12px', marginBottom: '20px' }}>
<div style={{
backgroundColor: isError ? '#d63638' : '#00a32a',
borderRadius: '50%',
width: '24px',
height: '24px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
marginTop: '2px'
}}>
<span style={{ color: 'white', fontSize: '14px', fontWeight: 'bold' }}>
{isError ? '✕' : '✓'}
</span>
</div>
<div>
<h3 style={{ margin: '0 0 8px 0', fontSize: '16px', fontWeight: '600' }}>
{title}
</h3>
<p style={{ margin: '0', color: '#666' }}>
{message}
{showSnippetsLink && (
<>
{' '}
<a href="admin.php?page=snippets" style={{ color: '#2271b1', textDecoration: 'none' }}>
{__('Code Snippets Library', 'code-snippets')}
</a>.
</>
)}
</p>
</div>
</ImportCard>
)
}

View File

@@ -0,0 +1,4 @@
export { ImporterSelector } from './ImporterSelector'
export { ImportOptions } from './ImportOptions'
export { SimpleSnippetTable } from './SimpleSnippetTable'
export { StatusDisplay } from './StatusDisplay'

View File

@@ -0,0 +1,3 @@
export { useImporterSelection } from './useImporterSelection'
export { useSnippetImport } from './useSnippetImport'
export { useImportSnippetSelection } from './useImportSnippetSelection'

View File

@@ -0,0 +1,45 @@
import { useState } from 'react'
import type { ImportableSnippet } from '../../../../hooks/useImportersAPI'
export const useImportSnippetSelection = (availableSnippets: ImportableSnippet[]) => {
const [selectedSnippets, setSelectedSnippets] = useState<Set<number>>(new Set())
const handleSnippetToggle = (snippetId: number) => {
const newSelected = new Set(selectedSnippets)
if (newSelected.has(snippetId)) {
newSelected.delete(snippetId)
} else {
newSelected.add(snippetId)
}
setSelectedSnippets(newSelected)
}
const handleSelectAll = () => {
if (selectedSnippets.size === availableSnippets.length) {
setSelectedSnippets(new Set())
} else {
setSelectedSnippets(new Set(availableSnippets.map(snippet => snippet.table_data.id)))
}
}
const clearSelection = () => {
setSelectedSnippets(new Set())
}
const getSelectedSnippets = () => {
return availableSnippets.filter(snippet =>
selectedSnippets.has(snippet.table_data.id)
)
}
const isAllSelected = selectedSnippets.size === availableSnippets.length && availableSnippets.length > 0
return {
selectedSnippets,
handleSnippetToggle,
handleSelectAll,
clearSelection,
getSelectedSnippets,
isAllSelected
}
}

View File

@@ -0,0 +1,42 @@
import { useState, useEffect } from 'react'
import { useImportersAPI, type Importer } from '../../../../hooks/useImportersAPI'
export const useImporterSelection = () => {
const [importers, setImporters] = useState<Importer[]>([])
const [selectedImporter, setSelectedImporter] = useState<string>('')
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [tagValue, setTagValue] = useState<string>('')
const importersAPI = useImportersAPI()
useEffect(() => {
const fetchImporters = async () => {
try {
const response = await importersAPI.fetchAll()
setImporters(response.data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error')
} finally {
setIsLoading(false)
}
}
fetchImporters()
}, [importersAPI])
const handleImporterChange = (newImporter: string) => {
setSelectedImporter(newImporter)
setTagValue(`imported-${newImporter}`)
}
return {
importers,
selectedImporter,
isLoading,
error,
tagValue,
setTagValue,
handleImporterChange
}
}

View File

@@ -0,0 +1,107 @@
import { useState } from 'react'
import { __ } from '@wordpress/i18n'
import { useImportersAPI, type ImportableSnippet } from '../../../../hooks/useImportersAPI'
import { isNetworkAdmin } from '../../../../utils/screen'
export const useSnippetImport = () => {
const [snippets, setSnippets] = useState<ImportableSnippet[]>([])
const [isLoadingSnippets, setIsLoadingSnippets] = useState(false)
const [snippetsError, setSnippetsError] = useState<string | null>(null)
const [isImporting, setIsImporting] = useState(false)
const [importError, setImportError] = useState<string | null>(null)
const [importSuccess, setImportSuccess] = useState<number[]>([])
const importersAPI = useImportersAPI()
const loadSnippets = async (importerName: string): Promise<boolean> => {
if (!importerName) {
alert(__('Please select an importer.', 'code-snippets'))
return false
}
setIsLoadingSnippets(true)
setSnippetsError(null)
setSnippets([])
clearResults()
try {
const response = await importersAPI.fetchSnippets(importerName)
setSnippets(response.data)
return true
} catch (err) {
setSnippetsError(err instanceof Error ? err.message : 'Unknown error')
return false
} finally {
setIsLoadingSnippets(false)
}
}
const importSnippets = async (
importerName: string,
selectedSnippetIds: number[],
autoAddTags: boolean,
tagValue: string
): Promise<boolean> => {
if (selectedSnippetIds.length === 0) {
alert(__('Please select snippets to import.', 'code-snippets'))
return false
}
if (!importerName) {
alert(__('Please select an importer.', 'code-snippets'))
return false
}
setIsImporting(true)
setImportError(null)
setImportSuccess([])
try {
const response = await importersAPI.importSnippets(importerName, {
ids: selectedSnippetIds,
network: isNetworkAdmin(),
auto_add_tags: autoAddTags,
tag_value: autoAddTags ? tagValue : undefined
})
setImportSuccess(response.data.imported)
if (response.data.imported.length > 0) {
setSnippets([])
return true
} else {
alert(__('No snippets were imported.', 'code-snippets'))
return false
}
} catch (err) {
setImportError(err instanceof Error ? err.message : 'Unknown error')
return false
} finally {
setIsImporting(false)
}
}
const clearResults = () => {
setImportSuccess([])
setImportError(null)
}
const resetAll = () => {
setSnippets([])
clearResults()
setSnippetsError(null)
}
return {
snippets,
isLoadingSnippets,
snippetsError,
isImporting,
importError,
importSuccess,
loadSnippets,
importSnippets,
clearResults,
resetAll
}
}

View File

@@ -0,0 +1 @@
export * from './ImportForm'

View File

@@ -0,0 +1,62 @@
import React, { useState, useEffect } from 'react'
import { __ } from '@wordpress/i18n'
import { FileUploadForm } from './FromFileUpload/FileUploadForm'
import { ImportForm } from './FromOtherPlugins/ImportForm'
import { ImportSection } from './shared'
type TabType = 'upload' | 'plugins'
export const ImportApp: React.FC = () => {
const [activeTab, setActiveTab] = useState<TabType>('upload')
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search)
const tabParam = urlParams.get('tab') as TabType
if (tabParam === 'plugins' || tabParam === 'upload') {
setActiveTab(tabParam)
}
}, [])
const handleTabChange = (tab: TabType) => {
setActiveTab(tab)
const url = new URL(window.location.href)
url.searchParams.set('tab', tab)
window.history.replaceState({}, '', url)
}
return (
<div className="narrow" style={{ maxWidth: '800px' }}>
<h2 className="nav-tab-wrapper" style={{ marginBottom: '20px' }}>
<a
className={`nav-tab${activeTab === 'upload' ? ' nav-tab-active' : ''}`}
href="#"
onClick={(e) => {
e.preventDefault()
handleTabChange('upload')
}}
>
{__('Import Snippets', 'code-snippets')}
</a>
<a
className={`nav-tab${activeTab === 'plugins' ? ' nav-tab-active' : ''}`}
href="#"
onClick={(e) => {
e.preventDefault()
handleTabChange('plugins')
}}
>
{__('Import from other plugins', 'code-snippets')}
</a>
</h2>
<ImportSection active={activeTab === 'upload'}>
<FileUploadForm />
</ImportSection>
<ImportSection active={activeTab === 'plugins'}>
<ImportForm />
</ImportSection>
</div>
)
}

View File

@@ -0,0 +1,45 @@
import React from 'react'
import classnames from 'classnames'
import type { HTMLAttributes } from 'react'
export interface ImportCardProps extends Omit<HTMLAttributes<HTMLDivElement>, 'className'> {
children: React.ReactNode
className?: string
variant?: 'default' | 'controls'
}
export const ImportCard = React.forwardRef<HTMLDivElement, ImportCardProps>(({
children,
className,
variant = 'default',
style,
...props
}, ref) => {
const cardStyle: React.CSSProperties = {
backgroundColor: '#ffffff',
padding: '25px',
borderRadius: '5px',
border: '1px solid #e0e0e0',
marginBottom: '10px',
width: '100%',
...style
}
return (
<div
ref={ref}
className={classnames(
{
'import-controls': variant === 'controls'
},
className
)}
style={cardStyle}
{...props}
>
{children}
</div>
)
})
ImportCard.displayName = 'ImportCard'

View File

@@ -0,0 +1,33 @@
import React from 'react'
import type { HTMLAttributes } from 'react'
export interface ImportSectionProps extends Omit<HTMLAttributes<HTMLDivElement>, 'style'> {
children: React.ReactNode
active?: boolean
className?: string
style?: React.CSSProperties
}
export const ImportSection: React.FC<ImportSectionProps> = ({
children,
active = false,
className,
style,
...props
}) => {
const sectionStyle: React.CSSProperties = {
display: active ? 'block' : 'none',
paddingTop: 0,
...style
}
return (
<div
className={className}
style={sectionStyle}
{...props}
>
{children}
</div>
)
}

View File

@@ -0,0 +1,4 @@
export { ImportCard } from './ImportCard'
export type { ImportCardProps } from './ImportCard'
export { ImportSection } from './ImportSection'
export type { ImportSectionProps } from './ImportSection'

View File

@@ -0,0 +1 @@
export * from './components'

View File

@@ -0,0 +1,192 @@
import React, { useState } from 'react'
import classnames from 'classnames'
import { __ } from '@wordpress/i18n'
import { addQueryArgs } from '@wordpress/url'
import { WithRestAPIContext } from '../../hooks/useRestAPI'
import { WithSnippetsListContext, useSnippetsList } from '../../hooks/useSnippetsList'
import { SubmitSnippetAction, useSubmitSnippet } from '../../hooks/useSubmitSnippet'
import { handleUnknownError } from '../../utils/errors'
import { createSnippetObject, getSnippetType, isCondition, validateSnippet } from '../../utils/snippets/snippets'
import { WithSnippetFormContext, useSnippetForm } from '../../hooks/useSnippetForm'
import { ConfirmDialog } from '../common/ConfirmDialog'
import { UpsellDialog } from '../common/UpsellDialog'
import { EditorSidebar } from '../EditorSidebar'
import { UpsellBanner } from '../common/UpsellBanner'
import { SnippetTypeInput } from './fields/SnippetTypeInput'
import { TagsEditor } from './fields/TagsEditor'
import { CodeEditor } from './fields/CodeEditor'
import { DescriptionEditor } from './fields/DescriptionEditor'
import { NameInput } from './fields/NameInput'
import { PageHeading } from './page/PageHeading'
import type { PropsWithChildren } from 'react'
import type { Snippet } from '../../types/Snippet'
const editFormClassName = ({ snippet, isReadOnly, isExpanded }: {
snippet: Snippet,
isReadOnly: boolean,
isExpanded: boolean
}) =>
classnames(
'snippet-form',
isExpanded ? 'snippet-form-expanded' : 'snippet-form-collapsed',
`${snippet.scope}-snippet`,
`${getSnippetType(snippet)}-snippet`,
`${snippet.id ? 'saved' : 'new'}-snippet`,
`${snippet.active ? 'active' : 'inactive'}-snippet`,
{
'erroneous-snippet': !!snippet.code_error,
'read-only-snippet': isReadOnly
}
)
interface ConfirmSubmitDialogProps {
doSubmit: (action: SubmitSnippetAction | undefined) => void
submitAction: SubmitSnippetAction | undefined
setSubmitAction: (action: SubmitSnippetAction | undefined) => void
validationWarning: string | undefined
setValidationWarning: (warning: string | undefined) => void
}
const ConfirmSubmitDialog: React.FC<ConfirmSubmitDialogProps> = ({
doSubmit,
submitAction,
setSubmitAction,
validationWarning,
setValidationWarning
}) =>
<ConfirmDialog
open={validationWarning !== undefined}
title={__('Snippet incomplete', 'code-snippets')}
confirmLabel={__('Continue', 'code-snippets')}
onCancel={() => {
setSubmitAction(undefined)
setValidationWarning(undefined)
}}
onConfirm={() => {
doSubmit(submitAction)
setSubmitAction(undefined)
setValidationWarning(undefined)
}}
>
<p>{`${validationWarning} ${__('Continue?', 'code-snippets')}`}</p>
</ConfirmDialog>
interface EditFormProps extends PropsWithChildren {
className?: string
}
const EditForm: React.FC<EditFormProps> = ({ children, className }) => {
const { submitSnippet } = useSubmitSnippet()
const { snippet } = useSnippetForm()
const { refreshSnippetsList } = useSnippetsList()
const [validationWarning, setValidationWarning] = useState<string | undefined>()
const [submitAction, setSubmitAction] = useState<SubmitSnippetAction | undefined>()
const doSubmit = (action?: SubmitSnippetAction) => {
submitSnippet(action)
.then(response => {
if (response && 0 !== response.id && window.CODE_SNIPPETS) {
if (window.location.href.toString().includes(window.CODE_SNIPPETS.urls.addNew)) {
document.title = document.title
.replace(__('Add New Snippet', 'code-snippets'), __('Edit Snippet', 'code-snippets'))
.replace(__('Add New Condition', 'code-snippets'), __('Edit Condition', 'code-snippets'))
const newUrl = addQueryArgs(window.CODE_SNIPPETS.urls.edit, { id: response.id })
window.history.pushState({}, document.title, newUrl)
}
}
})
.then(refreshSnippetsList)
.catch(handleUnknownError)
}
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const action = Object.values(SubmitSnippetAction).find(actionName =>
actionName === document.activeElement?.getAttribute('name'))
const validationWarning = validateSnippet(snippet)
if (validationWarning) {
setValidationWarning(validationWarning)
setSubmitAction(action)
} else {
doSubmit(action)
}
}
return (
<>
<form id="snippet-form" method="post" onSubmit={handleSubmit} className={className}>
{children}
</form>
<ConfirmSubmitDialog {...{ doSubmit, submitAction, setSubmitAction, validationWarning, setValidationWarning }} />
</>
)
}
const ConditionsEditor: React.FC = () => {
const { snippet } = useSnippetForm()
return isCondition(snippet)
? <div id="snippet_conditions" className="snippet-condition-editor-container">
<p>{__('This snippet type is not supported in this version of Code Snippets.')}</p>
</div>
: null
}
const EditFormWrap: React.FC = () => {
const { snippet, isReadOnly } = useSnippetForm()
const [isExpanded, setIsExpanded] = useState(false)
const [isUpgradeDialogOpen, setIsUpgradeDialogOpen] = useState(false)
return (
<div className="wrap">
<p><small className="cs-back">
{isCondition(snippet)
? <a href={addQueryArgs(window.CODE_SNIPPETS?.urls.manage, { type: 'cond' })}>
{__('Back to all conditions', 'code-snippets')}
</a>
: <a href={window.CODE_SNIPPETS?.urls.manage}>
{__('Back to all snippets', 'code-snippets')}
</a>}
</small></p>
<PageHeading />
<EditForm className={editFormClassName({ snippet, isReadOnly, isExpanded })}>
<main className="snippet-form-upper">
<div className="snippet-name-wrapper">
<NameInput />
<SnippetTypeInput setIsUpgradeDialogOpen={setIsUpgradeDialogOpen} />
</div>
<CodeEditor {...{ isExpanded, setIsExpanded }} />
<ConditionsEditor />
</main>
<div className="snippet-form-lower">
<UpsellBanner />
<DescriptionEditor />
<TagsEditor />
</div>
<EditorSidebar setIsUpgradeDialogOpen={setIsUpgradeDialogOpen} />
</EditForm>
<UpsellDialog isOpen={isUpgradeDialogOpen} setIsOpen={setIsUpgradeDialogOpen} />
</div>
)
}
export const SnippetForm: React.FC = () =>
<WithRestAPIContext>
<WithSnippetsListContext>
<WithSnippetFormContext initialSnippet={() => createSnippetObject(window.CODE_SNIPPETS_EDIT?.snippet)}>
<EditFormWrap />
</WithSnippetFormContext>
</WithSnippetsListContext>
</WithRestAPIContext>

View File

@@ -0,0 +1,94 @@
import React, { useEffect, useRef } from 'react'
import { __ } from '@wordpress/i18n'
import { useSubmitSnippet } from '../../../hooks/useSubmitSnippet'
import { handleUnknownError } from '../../../utils/errors'
import { isMacOS } from '../../../utils/screen'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
import { Button } from '../../common/Button'
import { ExpandIcon } from '../../common/icons/ExpandIcon'
import { MinimiseIcon } from '../../common/icons/MinimiseIcon'
import { CodeEditorShortcuts } from './CodeEditorShortcuts'
import type { Dispatch, RefObject, SetStateAction } from 'react'
interface EditorTextareaProps {
textareaRef: RefObject<HTMLTextAreaElement>
}
const EditorTextarea: React.FC<EditorTextareaProps> = ({ textareaRef }) => {
const { snippet, setSnippet } = useSnippetForm()
return (
<div className="snippet-editor">
<textarea
ref={textareaRef}
id="snippet-code"
name="snippet_code"
value={snippet.code}
rows={200}
spellCheck={false}
onChange={event => {
setSnippet(previous => ({ ...previous, code: event.target.value }))
}}
/>
<CodeEditorShortcuts editorTheme={window.CODE_SNIPPETS_EDIT?.editorTheme ?? 'default'} />
</div>
)
}
export interface CodeEditorProps {
isExpanded: boolean
setIsExpanded: Dispatch<SetStateAction<boolean>>
}
export const CodeEditor: React.FC<CodeEditorProps> = ({ isExpanded, setIsExpanded }) => {
const { snippet, setSnippet, codeEditorInstance, setCodeEditorInstance } = useSnippetForm()
const { submitSnippet } = useSubmitSnippet()
const textareaRef = useRef<HTMLTextAreaElement>(null)
useEffect(() => {
setCodeEditorInstance(editorInstance => {
if (textareaRef.current && !editorInstance) {
editorInstance = window.wp.codeEditor.initialize(textareaRef.current)
editorInstance.codemirror.on('changes', instance => {
setSnippet(previous => ({ ...previous, code: instance.getValue() }))
})
}
return editorInstance
})
}, [setCodeEditorInstance, textareaRef, setSnippet])
useEffect(() => {
if (codeEditorInstance) {
const extraKeys = codeEditorInstance.codemirror.getOption('extraKeys') ?? {}
const controlKey = isMacOS() ? 'Cmd' : 'Ctrl'
const onSave = () => {
submitSnippet()
.then(() => undefined)
.catch(handleUnknownError)
}
codeEditorInstance.codemirror.setOption('extraKeys', {
...'object' === typeof extraKeys ? extraKeys : undefined,
[`${controlKey}-S`]: onSave,
[`${controlKey}-Enter`]: onSave
})
}
}, [submitSnippet, codeEditorInstance, snippet])
return (
<div className="snippet-code-container">
<div className="above-snippet-code">
<h2><label htmlFor="snippet-code">{__('Snippet Content', 'code-snippets')}</label></h2>
<Button small className="expand-editor-button" onClick={() => setIsExpanded(current => !current)}>
{isExpanded ? <MinimiseIcon /> : <ExpandIcon />}
{isExpanded ? __('Minimize', 'code-snippets') : __('Expand', 'code-snippets')}
</Button>
</div>
<EditorTextarea textareaRef={textareaRef} />
</div>
)
}

View File

@@ -0,0 +1,123 @@
import { __, _x } from '@wordpress/i18n'
import classnames from 'classnames'
import React from 'react'
import { KEYBOARD_KEYS } from '../../../types/KeyboardShortcut'
import { isMacOS } from '../../../utils/screen'
import type { KeyboardKey, KeyboardShortcut } from '../../../types/KeyboardShortcut'
const shortcuts: Record<string, KeyboardShortcut> = {
saveChanges: {
label: __('Save changes', 'code-snippets'),
mod: 'Cmd',
key: 'S'
},
selectAll: {
label: __('Select all', 'code-snippets'),
mod: 'Cmd',
key: 'A'
},
beginSearch: {
label: __('Begin searching', 'code-snippets'),
mod: 'Cmd',
key: 'F'
},
findNext: {
label: __('Find next', 'code-snippets'),
mod: 'Cmd',
key: 'G'
},
findPrevious: {
label: __('Find previous', 'code-snippets'),
mod: ['Shift', 'Cmd'],
key: 'G'
},
replace: {
label: __('Replace', 'code-snippets'),
mod: ['Shift', 'Cmd'],
key: 'F'
},
replaceAll: {
label: __('Replace all', 'code-snippets'),
mod: ['Shift', 'Cmd', 'Option'],
key: 'R'
},
search: {
label: __('Persistent search', 'code-snippets'),
mod: 'Alt',
key: 'F'
},
toggleComment: {
label: __('Toggle comment', 'code-snippets'),
mod: 'Cmd',
key: '/'
},
swapLineUp: {
label: __('Swap line up', 'code-snippets'),
mod: 'Option',
key: 'Up'
},
swapLineDown: {
label: __('Swap line down', 'code-snippets'),
mod: 'Option',
key: 'Down'
},
autoIndent: {
label: __('Auto-indent current line or selection', 'code-snippets'),
mod: 'Shift',
key: 'Tab'
}
}
const SEP = _x('-', 'keyboard shortcut separator', 'code-snippets')
const ModifierKey: React.FC<{ modifier: KeyboardKey }> = ({ modifier }) => {
switch (modifier) {
case 'Ctrl':
case 'Cmd':
return (
<>
<kbd className="pc-key">{KEYBOARD_KEYS.Ctrl}</kbd>
<kbd className="mac-key">{KEYBOARD_KEYS.Cmd}</kbd>
{SEP}
</>
)
case 'Option':
return (
<span className="mac-key">
<kbd className="mac-key">{KEYBOARD_KEYS.Option}</kbd>{SEP}
</span>
)
default:
return <><kbd>{KEYBOARD_KEYS[modifier]}</kbd>{SEP}</>
}
}
export interface CodeEditorShortcutsProps {
editorTheme: string
}
export const CodeEditorShortcuts: React.FC<CodeEditorShortcutsProps> = ({ editorTheme }) =>
<div className="snippet-editor-help tooltip tooltip-inline tooltip-start">
<span className={`dashicons dashicons-editor-help cm-s-${editorTheme}`}></span>
<div className={classnames('tooltip-content', { 'platform-mac': isMacOS() })}>
<table>
<tbody>
{Object.entries(shortcuts).map(([name, { label, mod, key }]) =>
<tr key={name}>
<td>{label}</td>
<td>
{(Array.isArray(mod) ? mod : [mod]).map(modifier =>
<span key={modifier}>
<ModifierKey modifier={modifier} />
</span>
)}
<kbd>{KEYBOARD_KEYS[key]}</kbd>
</td>
</tr>)}
</tbody>
</table>
</div>
</div>

View File

@@ -0,0 +1,88 @@
import React, { useCallback, useEffect } from 'react'
import { __ } from '@wordpress/i18n'
import domReady from '@wordpress/dom-ready'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
export const EDITOR_ID = 'snippet_description'
const TOOLBAR_BUTTONS = [
[
'bold',
'italic',
'underline',
'strikethrough',
'blockquote',
'bullist',
'numlist',
'alignleft',
'aligncenter',
'alignright',
'link',
'wp_adv',
'code_snippets'
],
[
'formatselect',
'forecolor',
'pastetext',
'removeformat',
'charmap',
'outdent',
'indent',
'undo',
'redo',
'spellchecker'
]
]
const initializeEditor = (onChange: (content: string) => void) => {
window.wp.editor?.initialize(EDITOR_ID, {
mediaButtons: window.CODE_SNIPPETS_EDIT?.descEditorOptions.mediaButtons,
quicktags: true,
tinymce: {
toolbar: TOOLBAR_BUTTONS.map(line => line.join(' ')),
setup: editor => {
editor.on('change', () => onChange(editor.getContent()))
}
}
})
}
const DescriptionEditorTextarea: React.FC = () => {
const { snippet, setSnippet, isReadOnly } = useSnippetForm()
const handleChange = useCallback(
(desc: string) => setSnippet(previous => ({ ...previous, desc })),
[setSnippet]
)
useEffect(() => {
domReady(() => initializeEditor(handleChange))
}, [handleChange])
return (
<textarea
id={EDITOR_ID}
className="wp-editor-area"
onChange={event => handleChange(event.target.value)}
autoComplete="off"
disabled={isReadOnly}
rows={window.CODE_SNIPPETS_EDIT?.descEditorOptions.rows}
cols={40}
value={snippet.desc}
/>
)
}
export const DescriptionEditor: React.FC = () =>
window.CODE_SNIPPETS_EDIT?.enableDescription
? <div className="snippet-description-container">
<h2>
<label htmlFor={EDITOR_ID}>
{__('Description', 'code-snippets')}
</label>
</h2>
<DescriptionEditorTextarea />
</div>
: null

View File

@@ -0,0 +1,28 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
export const NameInput: React.FC = () => {
const { snippet, setSnippet, isReadOnly } = useSnippetForm()
return (
<div id="titlediv">
<div id="titlewrap">
<label htmlFor="title" className="screen-reader-text">
{__('Name', 'code-snippets')}
</label>
<input
id="title"
type="text"
name="snippet_name"
autoComplete="off"
value={snippet.name}
disabled={isReadOnly}
placeholder={__('Enter snippet title', 'code-snippets')}
onChange={event =>
setSnippet(previous => ({ ...previous, name: event.target.value }))}
/>
</div>
</div>
)
}

View File

@@ -0,0 +1,72 @@
import { __ } from '@wordpress/i18n'
import React from 'react'
import Select from 'react-select'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
import { SNIPPET_TYPE_SCOPES } from '../../../types/Snippet'
import { getSnippetType, isCondition } from '../../../utils/snippets/snippets'
import type { SnippetCodeScope } from '../../../types/Snippet'
import type { SelectOption } from '../../../types/SelectOption'
const SCOPE_ICONS: Record<SnippetCodeScope, string> = {
'global': 'admin-site',
'admin': 'admin-tools',
'front-end': 'admin-appearance',
'single-use': 'clock',
'content': 'shortcode',
'head-content': 'editor-code',
'footer-content': 'editor-code',
'admin-css': 'dashboard',
'site-css': 'admin-customizer',
'site-head-js': 'media-code',
'site-footer-js': 'media-code'
}
const SCOPE_DESCRIPTIONS: Record<SnippetCodeScope, string> = {
'global': __('Run everywhere', 'code-snippets'),
'admin': __('Only run in administration area', 'code-snippets'),
'front-end': __('Only run on site front-end', 'code-snippets'),
'single-use': __('Only run once', 'code-snippets'),
'content': __('Where inserted in editor', 'code-snippets'),
'head-content': __('In site <head> section', 'code-snippets'),
'footer-content': __('In site footer (end of <body>)', 'code-snippets'),
'site-css': __('Site front-end', 'code-snippets'),
'admin-css': __('Administration area', 'code-snippets'),
'site-footer-js': __('In site footer (end of <body>)', 'code-snippets'),
'site-head-js': __('In site <head> section', 'code-snippets')
}
export const SnippetLocationInput: React.FC = () => {
const { snippet, setSnippet, isReadOnly } = useSnippetForm()
const options: SelectOption<SnippetCodeScope>[] = SNIPPET_TYPE_SCOPES[getSnippetType(snippet)]
.filter(scope => 'condition' !== scope)
.map(scope => ({
key: scope,
value: scope,
label: SCOPE_DESCRIPTIONS[scope]
}))
return isCondition(snippet)
? null
: <div className="block-form-field">
<h4><label htmlFor="snippet-location">{__('Location', 'code-snippets')}</label></h4>
<Select
inputId="snippet-location"
className="code-snippets-select code-snippets-select-location"
options={options}
isDisabled={isReadOnly}
styles={{
menu: provided => ({ ...provided, zIndex: 9999 }),
input: provided => ({ ...provided, ':focus': { boxShadow: 'none' } })
}}
value={options.find(option => option.value === snippet.scope)}
formatOptionLabel={({ label, value }) =>
<>
<span className={`dashicons dashicons-${SCOPE_ICONS[value]}`}></span>{` ${label}`}
</>
}
onChange={option =>
option?.value && setSnippet(previous => ({ ...previous, scope: option.value }))}
/>
</div>
}

View File

@@ -0,0 +1,106 @@
import React, { useEffect } from 'react'
import classnames from 'classnames'
import { __, _x } from '@wordpress/i18n'
import Select from 'react-select'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
import { SNIPPET_TYPE_SCOPES } from '../../../types/Snippet'
import { isLicensed } from '../../../utils/screen'
import { getSnippetType, isProType } from '../../../utils/snippets/snippets'
import { Badge } from '../../common/Badge'
import type { FormatOptionLabelContext } from 'react-select'
import type { Dispatch, SetStateAction } from 'react'
import type { SnippetCodeType, SnippetType } from '../../../types/Snippet'
import type { SelectOption } from '../../../types/SelectOption'
import type { EditorConfiguration } from 'codemirror'
export interface SnippetTypeInputProps {
setIsUpgradeDialogOpen: Dispatch<SetStateAction<boolean>>
}
const EDITOR_MODES: Record<SnippetCodeType, string> = {
css: 'text/css',
js: 'javascript',
php: 'text/x-php',
html: 'application/x-httpd-php'
}
const OPTIONS: SelectOption<SnippetType>[] = [
{ value: 'php', label: __('Functions', 'code-snippets') },
{ value: 'html', label: __('Content', 'code-snippets') },
{ value: 'css', label: __('Styles', 'code-snippets') },
{ value: 'js', label: __('Scripts', 'code-snippets') },
{ value: 'cond', label: __('Conditions', 'code-snippets') }
]
interface SnippetTypeOptionProps {
option: SelectOption<SnippetType>
context: FormatOptionLabelContext
}
const SnippetTypeOption: React.FC<SnippetTypeOptionProps> = ({ option: { value, label }, context }) =>
<div className={classnames('snippet-type-option', { 'inverted-badges': isProType(value) && !isLicensed() })}>
{'menu' === context
? <div>
{label}
{isProType(value) && !isLicensed()
? <Badge name="pro" small>{_x('Pro', 'Upgrade to Pro', 'code-snippets')}</Badge>
: null}
</div>
: null}
<Badge name={value} />
</div>
export const SnippetTypeInput: React.FC<SnippetTypeInputProps> = ({ setIsUpgradeDialogOpen }) => {
const { snippet, setSnippet, codeEditorInstance, isReadOnly } = useSnippetForm()
const snippetType = getSnippetType(snippet)
useEffect(() => {
if (codeEditorInstance) {
const codeEditor = codeEditorInstance.codemirror
codeEditor.setOption('lint' as keyof EditorConfiguration, 'php' === snippetType || 'css' === snippetType)
if ('cond' !== snippetType && EDITOR_MODES[snippetType]) {
codeEditor.setOption('mode', EDITOR_MODES[snippetType])
codeEditor.refresh()
}
}
}, [codeEditorInstance, snippetType])
return (
<div className="snippet-type-container">
<label htmlFor="snippet-type-select-input" className="screen-reader-text">
{__('Snippet Type', 'code-snippets')}
</label>
<Select
inputId="snippet-type-select-input"
className="code-snippets-select"
isDisabled={isReadOnly}
options={0 === snippet.id ? OPTIONS : OPTIONS.filter(option => 'cond' !== option.value)}
menuPlacement="bottom"
styles={{
menu: provided => ({
...provided,
zIndex: 9999,
width: 'max-content',
minWidth: '100%'
}),
input: provided => ({ ...provided, boxShadow: 'none' })
}}
value={OPTIONS.find(option => option.value === snippetType)}
formatOptionLabel={(data, meta) =>
<SnippetTypeOption option={data} context={meta.context} />}
onChange={option => {
if (option && isProType(option.value) && !isLicensed()) {
setIsUpgradeDialogOpen(true)
} else if (option) {
setSnippet(previous => ({
...previous,
scope: SNIPPET_TYPE_SCOPES[option.value][0]
}))
}
}}
/>
</div>
)
}

View File

@@ -0,0 +1,31 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { FormTokenField } from '@wordpress/components'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
const options = window.CODE_SNIPPETS_EDIT?.tagOptions
export const TagsEditor: React.FC = () => {
const { snippet, setSnippet, isReadOnly } = useSnippetForm()
return options?.enabled
? <div className="snippet-tags-container">
<h3><label htmlFor="components-form-token-input-0">{__('Snippet Tags', 'code-snippets')}</label></h3>
<FormTokenField
label=""
value={snippet.tags}
disabled={isReadOnly}
suggestions={options.availableTags}
tokenizeOnBlur
tokenizeOnSpace={!options.allowSpaces}
onChange={tokens => {
setSnippet(previous => ({
...previous,
tags: tokens.map(token => 'string' === typeof token ? token : token.value)
}))
}}
/>
</div>
: null
}

View File

@@ -0,0 +1 @@
export * from './SnippetForm'

View File

@@ -0,0 +1,34 @@
import { createInterpolateElement } from '@wordpress/element'
import React from 'react'
import { __, sprintf } from '@wordpress/i18n'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
import { DismissibleNotice } from '../../common/DismissableNotice'
export const Notices: React.FC = () => {
const { currentNotice, setCurrentNotice, snippet, setSnippet } = useSnippetForm()
return <>
{currentNotice
? <DismissibleNotice className={currentNotice[0]} onDismiss={() => setCurrentNotice(undefined)}>
<p>{createInterpolateElement(currentNotice[1], { strong: <strong /> })}</p>
</DismissibleNotice>
: null}
{snippet.code_error
? <DismissibleNotice
className="notice-error"
onDismiss={() => setSnippet(previous => ({ ...previous, code_error: null }))}
>
<p>
<strong>{sprintf(
// translators: %d: line number.
__('Snippet automatically deactivated due to an error on line %d:', 'code-snippets'),
snippet.code_error[1]
)}</strong>
<blockquote>{snippet.code_error[0]}</blockquote>
</p>
</DismissibleNotice>
: null}
</>
}

View File

@@ -0,0 +1,52 @@
import { __, _x } from '@wordpress/i18n'
import React from 'react'
import { useSnippetForm } from '../../../hooks/useSnippetForm'
import { createSnippetObject } from '../../../utils/snippets/snippets'
import type { Snippet } from '../../../types/Snippet'
const OPTIONS = window.CODE_SNIPPETS_EDIT
const getAddNewHeading = (snippet: Snippet): string =>
'condition' === snippet.scope
? __('Add New Condition', 'code-snippets')
: __('Add New Snippet', 'code-snippets')
export const PageHeading: React.FC = () => {
const { snippet, updateSnippet, setCurrentNotice } = useSnippetForm()
return (
<h1>
{snippet.id
? <>
{`${'condition' === snippet.scope
? __('Edit Condition', 'code-snippets')
: __('Edit Snippet', 'code-snippets')} `}
<a
href={window.CODE_SNIPPETS?.urls.addNew}
className="page-title-action"
onClick={event => {
event.preventDefault()
updateSnippet(({ scope }) => createSnippetObject({ scope }))
setCurrentNotice(undefined)
window.document.title = window.document.title
.replace(__('Edit Snippet', 'code-snippets'), getAddNewHeading(snippet))
.replace(__('Edit Condition', 'code-snippets'), getAddNewHeading(snippet))
window.history.pushState({}, '', window.CODE_SNIPPETS?.urls.addNew)
}}
>
{_x('Add New', 'snippet', 'code-snippets')}
</a>
</>
: getAddNewHeading(snippet)}
{OPTIONS?.pageTitleActions && Object.entries(OPTIONS.pageTitleActions).map(([label, url]) =>
<>
<a key={label} href={url} className="page-title-action">{label}</a>{' '}
</>
)}
</h1>
)
}

View File

@@ -0,0 +1,27 @@
import React from 'react'
import classnames from 'classnames'
import type { ReactNode } from 'react'
import type { SnippetType } from '../../types/Snippet'
export type BadgeName = SnippetType | 'core' | 'pro' | 'ai' | 'cloud' | 'bundles' | 'cloud_search' | 'beta'
const badgeIcons: Partial<Record<BadgeName, string>> = {
cond: 'randomize',
cloud: 'cloud',
bundles: 'screenoptions',
cloud_search: 'search'
}
export interface BadgeProps {
name: BadgeName
small?: boolean
inverted?: boolean
children?: ReactNode
}
export const Badge: React.FC<BadgeProps> = ({ name, small, inverted, children }) =>
<span className={classnames('badge', `${name}-badge`, { 'small-badge': small, 'inverted-badge': inverted })}>
{badgeIcons[name]
? <span className={`dashicons dashicons-${badgeIcons[name]}`} />
: children ?? name}
</span>

View File

@@ -0,0 +1,49 @@
import React from 'react'
import classnames from 'classnames'
import type { ButtonHTMLAttributes } from 'react'
export interface ButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'id' | 'name'> {
id?: string
name?: string
primary?: boolean
secondary?: boolean
small?: boolean
large?: boolean
link?: boolean
}
export const Button: React.FC<ButtonProps> = ({
id,
children,
className,
name,
primary = false,
secondary = false,
small = false,
large = false,
link = false,
type = 'button',
onClick,
...props
}) =>
<button
id={id ?? name}
name={name}
type={type}
{...props}
onClick={event => {
if (onClick) {
event.preventDefault()
onClick(event)
}
}}
className={classnames('button', className, {
'button-primary': primary,
'button-secondary': secondary,
'button-large': large,
'button-small': small,
'button-link': link
})}
>
{children}
</button>

View File

@@ -0,0 +1,49 @@
import React from 'react'
import { __ } from '@wordpress/i18n'
import { Button, Flex, Modal } from '@wordpress/components'
import type { ReactNode } from 'react'
export interface ConfirmDialogProps {
open?: boolean
title: string
onConfirm?: VoidFunction
onCancel: VoidFunction
confirmLabel?: string
cancelLabel?: string
children?: ReactNode,
confirmButtonClassName?: string
}
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({
open,
title,
onConfirm,
onCancel,
children,
confirmLabel = __('OK', 'code-snippets'),
cancelLabel = __('Cancel', 'code-snippets'),
confirmButtonClassName
}) =>
open
? <Modal
title={title}
onRequestClose={onCancel}
closeButtonLabel={cancelLabel}
isDismissible={true}
onKeyDown={event => {
if ('Enter' === event.key) {
onConfirm?.()
}
}}
>
{children}
<Flex direction="row" justify="flex-end">
<Button variant="tertiary" onClick={onCancel}>
{cancelLabel}
</Button>
<Button variant="primary" onClick={onConfirm} className={confirmButtonClassName}>
{confirmLabel}
</Button>
</Flex>
</Modal>
: null

View File

@@ -0,0 +1,71 @@
import { Spinner } from '@wordpress/components'
import { __ } from '@wordpress/i18n'
import React, { useState } from 'react'
import { Button } from './Button'
import { CopyIcon } from './icons/CopyIcon'
import type { ButtonProps } from './Button'
const TIMEOUT = 1500
enum Status {
INITIAL,
PROGRESSING,
SUCCESS,
ERROR
}
interface StatusIconProps {
status: Status
}
const StatusIcon: React.FC<StatusIconProps> = ({ status }) => {
switch (status) {
case Status.INITIAL:
return <CopyIcon />
case Status.PROGRESSING:
return <span className="spinner-wrapper"><Spinner /></span>
case Status.SUCCESS:
return <span className="dashicons dashicons-yes"></span>
case Status.ERROR:
return <span className="dashicons dashicons-warning"></span>
}
}
export interface CopyToClipboardButtonProps extends ButtonProps {
text: string
timeout?: number
}
export const CopyToClipboardButton: React.FC<CopyToClipboardButtonProps> = ({
text,
timeout = TIMEOUT,
...props
}) => {
const [status, setStatus] = useState(Status.INITIAL)
const clipboard = window.navigator.clipboard as Clipboard | undefined
const handleClick = () => {
setStatus(Status.PROGRESSING)
clipboard?.writeText(text)
.then(() => {
setStatus(Status.SUCCESS)
setTimeout(() => setStatus(Status.INITIAL), timeout)
})
.catch((error: unknown) => {
console.error('Failed to copy text to clipboard.', error)
setStatus(Status.ERROR)
})
}
return clipboard && window.isSecureContext
? <Button
className="code-snippets-copy-text"
onClick={handleClick}
{...props}
>
<StatusIcon status={status} />
{__('Copy', 'code-snippets')}
</Button>
: null
}

View File

@@ -0,0 +1,22 @@
import { __ } from '@wordpress/i18n'
import classnames from 'classnames'
import React from 'react'
import type { ReactNode } from 'react'
export interface DismissibleNoticeProps {
className?: classnames.Argument
onDismiss: VoidFunction
children?: ReactNode
}
export const DismissibleNotice: React.FC<DismissibleNoticeProps> = ({ className, onDismiss, children }) =>
<div id="message" className={classnames('notice fade is-dismissible', className)}>
<>{children}</>
<button type="button" className="notice-dismiss" onClick={event => {
event.preventDefault()
onDismiss()
}}>
<span className="screen-reader-text">{__('Dismiss notice.', 'code-snippets')}</span>
</button>
</div>

View File

@@ -0,0 +1,46 @@
import React from 'react'
import classnames from 'classnames'
import { __ } from '@wordpress/i18n'
import type { InputHTMLAttributes } from 'react'
export interface SubmitButtonProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'id' | 'name' | 'value'> {
id?: string
name?: string
primary?: boolean
small?: boolean
large?: boolean
wrap?: boolean
text?: string
}
export const SubmitButton: React.FC<SubmitButtonProps> = ({
id,
text,
name = 'submit',
primary,
small,
large,
wrap,
className,
...inputProps
}) => {
const button =
<input
id={id ?? name}
type="submit"
name={name}
value={text ?? __('Save Changes', 'code-snippets')}
className={classnames(
'button',
{
'button-primary': primary,
'button-small': small,
'button-large': large
},
className
)}
{...inputProps}
/>
return wrap ? <p className="submit">{button}</p> : button
}

View File

@@ -0,0 +1,25 @@
import React from 'react'
import classnames from 'classnames'
import type { ReactNode } from 'react'
export interface TooltipProps {
block?: boolean
inline?: boolean
start?: boolean
end?: boolean
icon?: ReactNode
children: ReactNode
className?: classnames.Argument
}
export const Tooltip: React.FC<TooltipProps> = ({ block, inline, start, end, icon, className, children }) =>
<div className={classnames(
'tooltip',
{ 'tooltip-block': block, 'tooltip-inline': inline, 'tooltip-start': start, 'tooltip-end': end },
className
)}>
{icon ?? <span className="dashicons dashicons-editor-help"></span>}
<div className="tooltip-content">
{children}
</div>
</div>

View File

@@ -0,0 +1,37 @@
import { ExternalLink } from '@wordpress/components'
import { createInterpolateElement } from '@wordpress/element'
import { __ } from '@wordpress/i18n'
import React, { useState } from 'react'
import { isLicensed } from '../../utils/screen'
import { Button } from './Button'
export const UpsellBanner = () => {
const [isDismissed, setIsDismissed] = useState(false)
return isDismissed || isLicensed() || window.CODE_SNIPPETS_EDIT?.hideUpsell
? null
: <div className="code-snippets-upsell-banner">
<img
src={`${window.CODE_SNIPPETS?.urls.plugin}/assets/icon.svg`}
alt={__('Code Snippets logo', 'code-snippets')}
height="34"
/>
<p>
{createInterpolateElement(
__('Unlock <strong>cloud sync, snippet conditions, AI features</strong> and much more with Code Snippets Pro.', 'code-snippets'),
{ strong: <strong /> }
)}
</p>
<ExternalLink
className="button button-primary button-large"
href="https://codesnippets.pro/pricing/"
>
{__('Get Started', 'code-snippets')}
</ExternalLink>
<Button small link onClick={() => setIsDismissed(true)}>
<span className="dashicons dashicons-no-alt"></span>
</Button>
</div>
}

View File

@@ -0,0 +1,62 @@
import { createInterpolateElement } from '@wordpress/element'
import React from 'react'
import { __ } from '@wordpress/i18n'
import { Modal } from '@wordpress/components'
import type { Dispatch, SetStateAction } from 'react'
export interface UpsellDialogProps {
isOpen: boolean
setIsOpen: Dispatch<SetStateAction<boolean>>
}
export const UpsellDialog: React.FC<UpsellDialogProps> = ({ isOpen, setIsOpen }) =>
isOpen
? <Modal
title=""
className="code-snippets-upsell-dialog"
onRequestClose={() => {
setIsOpen(false)
}}
>
<img
src={`${window.CODE_SNIPPETS?.urls.plugin}/assets/icon.svg`}
alt={__('Code Snippets logo', 'code-snippets')}
/>
<h1>
{createInterpolateElement(
__('Unlock all cloud sync features and many more, with <span>Code Snippets Pro</span>', 'code-snippets'),
{ span: <span /> }
)}
</h1>
<p>
{createInterpolateElement(
__('With Code Snippets Pro you can connect your WordPress sites to the code snippets cloud platform and be able to <strong>backup, synchronise, collaborate, and deploy</strong> your snippets from one central location.', 'code-snippets'),
{ strong: <strong /> }
)}
</p>
<a
href="https://codesnippets.pro/pricing/"
className="button button-primary button-large"
rel="noreferrer" target="_blank"
>
{__('Explore Code Snippets Pro', 'code-snippets')}
</a>
<h2>{__("Here's what else you get with Pro:", 'code-snippets')}</h2>
<ul>
<li>{__('Create, explain and verify snippets with AI', 'code-snippets')}</li>
<li>{__('Control when snippets run with Conditions', 'code-snippets')}</li>
<li>{__('CSS stylesheet snippets', 'code-snippets')}</li>
<li>{__('Minified JavaScript snippets', 'code-snippets')}</li>
<li>{__('Editor blocks and Elementor widgets', 'code-snippets')}</li>
<li>{__('Cloud sync and backup', 'code-snippets')}</li>
<li>{__('Cloud share and deploy', 'code-snippets')}</li>
<li>{__('Cloud bundles and teams', 'code-snippets')}</li>
<li>{__('WP-CLI commands', 'code-snippets')}</li>
<li>{__('And much more!', 'code-snippets')}</li>
</ul>
</Modal>
: null

View File

@@ -0,0 +1,12 @@
import React from 'react'
export const CopyIcon = () =>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M12 6V4.5C12 4.10218 11.842 3.72064 11.5607 3.43934C11.2794 3.15804 10.8978 3 10.5 3H4.5C4.10218 3 3.72064 3.15804 3.43934 3.43934C3.15804 3.72064 3 4.10218 3 4.5V10.5C3 10.8978 3.15804 11.2794 3.43934 11.5607C3.72064 11.842 4.10218 12 4.5 12H6M6 7.5C6 7.10218 6.15804 6.72065 6.43934 6.43934C6.72065 6.15804 7.10218 6 7.5 6H13.5C13.8978 6 14.2794 6.15804 14.5607 6.43934C14.842 6.72065 15 7.10218 15 7.5V13.5C15 13.8978 14.842 14.2794 14.5607 14.5607C14.2794 14.842 13.8978 15 13.5 15H7.5C7.10218 15 6.72065 14.842 6.43934 14.5607C6.15804 14.2794 6 13.8978 6 13.5V7.5Z"
stroke="#F0F0F0"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>

View File

@@ -0,0 +1,20 @@
import React from 'react'
export const ExpandIcon = () =>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 3H15V6" />
<path d="M10.5 7.5L15 3L10.5 7.5Z" />
<path d="M6 15H3V12" />
<path d="M3 15L7.5 10.5L3 15Z" />
<path d="M12 15H15V12" />
<path d="M10.5 10.5L15 15L10.5 10.5Z" />
<path d="M6 3H3V6" />
<path d="M3 3L7.5 7.5L3 3Z" />
<path
d="M12 3H15M15 3V6M15 3L10.5 7.5M6 15H3M3 15V12M3 15L7.5 10.5M12 15H15M15 15V12M15 15L10.5 10.5M6 3H3M3 3V6M3 3L7.5 7.5"
stroke="currentcolor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>

View File

@@ -0,0 +1,20 @@
import React from 'react'
export const MinimiseIcon = () =>
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.75 6.75H6.75V3.75" />
<path d="M2.25 2.25L6.75 6.75L2.25 2.25Z" />
<path d="M3.75 11.25H6.75V14.25" />
<path d="M2.25 15.75L6.75 11.25L2.25 15.75Z" />
<path d="M14.25 6.75H11.25V3.75" />
<path d="M11.25 6.75L15.75 2.25L11.25 6.75Z" />
<path d="M14.25 11.25H11.25V14.25" />
<path d="M11.25 11.25L15.75 15.75L11.25 11.25Z" />
<path
d="M3.75 6.75H6.75M6.75 6.75V3.75M6.75 6.75L2.25 2.25M3.75 11.25H6.75M6.75 11.25V14.25M6.75 11.25L2.25 15.75M14.25 6.75H11.25M11.25 6.75V3.75M11.25 6.75L15.75 2.25M14.25 11.25H11.25M11.25 11.25V14.25M11.25 11.25L15.75 15.75"
stroke="currentcolor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>