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,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
}