import React, { useEffect, useState } from 'react'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; import { Input } from '@/components/ui/input'; import { Toaster } from "@/components/ui/sonner" import { toast } from "sonner" import { Alert, AlertDescription } from "@/components/ui/alert" import { InfoIcon } from "lucide-react"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Card, CardContent, CardFooter, } from "@/components/ui/card" import { Skeleton } from "@/components/ui/skeleton" import { Settings as SettingsIcon, Key as KeyIcon, CalendarSync as CalendarSyncIcon, Plug as PlugIcon } from "lucide-react"; import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; interface Field { /** Setting key; omitted only for non-persisted containers (not used). */ key?: string; type: string; label?: string; description?: string; default?: any; options?: { value: string; label: string }[]; switch_label?: string; min?: number; max?: number; step?: number; /** Nested fields (type `field_group`). */ fields?: Field[]; /** When true with `field_group`, children are shown in a bordered panel. */ boxed?: boolean; } interface Section { title?: string; description?: string; fields?: Field[]; content?: any[]; } interface Tab { key: string; label: string; icon?: string; sections?: Section[]; } interface Schema { tabs: Tab[]; } declare global { interface Window { worddown_variables?: { restUrl?: string; restNonce?: string; siteUrl?: string; mcpEndpoint?: string; strings?: Record; }; } } const API_BASE = (window.worddown_variables?.restUrl || '/wp-json/worddown/v1/').replace(/\/$/, ''); const REST_NONCE: string = window.worddown_variables?.restNonce || ''; // Custom translation function to use localized strings from PHP function __(text: string, domain = 'worddown') { if ( window.worddown_variables && window.worddown_variables.strings && window.worddown_variables.strings[text] ) { return window.worddown_variables.strings[text]; } return text; } export default function SettingsPanel() { const [schema, setSchema] = useState(null); const [settings, setSettings] = useState | null>(null); const [activeTab, setActiveTab] = useState('general'); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [postTypeOptions, setPostTypeOptions] = useState<{ value: string; label: string }[]>([]); const [adapters, setAdapters] = useState<{ slug: string; label: string; description?: string }[] | null>(null); useEffect(() => { async function fetchData() { try { const [schemaRes, settingsRes, postTypesRes, adaptersRes] = await Promise.all([ fetch(`${API_BASE}/settings-schema`, { headers: { 'X-WP-Nonce': REST_NONCE } }).then(r => r.json()), fetch(`${API_BASE}/settings`, { headers: { 'X-WP-Nonce': REST_NONCE } }).then(r => r.json()), fetch(`${API_BASE}/post-types`, { headers: { 'X-WP-Nonce': REST_NONCE } }).then(r => r.json()), fetch(`${API_BASE}/adapters`, { headers: { 'X-WP-Nonce': REST_NONCE } }).then(r => r.json()), ]); setSchema(schemaRes); setSettings(settingsRes); setPostTypeOptions(postTypesRes); setAdapters(adaptersRes); setLoading(false); } catch (e) { toast.error(__('Failed to save settings.', 'worddown')); setLoading(false); } } fetchData(); }, []); function handleChange(key: string, value: any) { setSettings(prev => ({ ...(prev || {}), [key]: value })); } function interpolateCodeTemplate(code: string): string { const siteUrl = window.worddown_variables?.siteUrl || window.location.origin; const mcpEndpoint = window.worddown_variables?.mcpEndpoint || `${siteUrl}/wp-json/mcp/mcp-adapter-default-server`; const apiKey = settings?.api_key ?? '{api_key}'; return code .replace(/\{site_url\}/g, siteUrl) .replace(/\{mcp_endpoint\}/g, mcpEndpoint) .replace(/\{api_key\}/g, apiKey || '{api_key}') .replace(/\{wp_username\}/g, '{wp_username}') .replace(/\{application_password\}/g, '{application_password}'); } async function handleSave(e: React.FormEvent) { e.preventDefault(); setSaving(true); try { const res = await fetch(`${API_BASE}/settings`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': REST_NONCE, } as HeadersInit, body: JSON.stringify(settings), }); const data = await res.json(); if (data.success) { toast.success(__('Settings saved!', 'worddown')); } else { if (data.error) { toast.error(data.error); } else { toast.error(__('Failed to save settings.', 'worddown')); } } } catch (e) { toast.error(__('Failed to save settings.', 'worddown')); } setSaving(false); } function renderSchemaField(field: Field, path: string): React.ReactNode { const vals = settings ?? {}; if (field.type === 'field_group' && field.fields?.length) { const gid = field.key ?? path; const inner = (
{field.fields.map((sub, i) => renderSchemaField(sub, `${gid}-${i}`))}
); return (
{field.description ? (
{field.description}
) : null} {field.boxed ? (
{inner}
) : ( inner )}
); } if (field.type === 'adapters') { if (!adapters) { return (
); } return (
{(!adapters || adapters.length === 0) ? ( {__('Supported adapters will be shown here automatically when they are available.', 'worddown')} ) : ( adapters.map(adapter => (
handleChange(`include_${adapter.slug}`, val)} id={`switch-include-${adapter.slug}`} /> {adapter.description && ( )}
)) )}
); } const leafKey = field.key; if (!leafKey) { return null; } return (
{field.type === 'boolean' && (
handleChange(leafKey, val)} id={`switch-${leafKey}`} />
)} {field.type === 'text' && ( handleChange(leafKey, e.target.value)} /> )} {field.type === 'number' && ( { const value = e.target.value; if (value === '') { handleChange(leafKey, field.default || 0); } else { const numValue = parseInt(value, 10); if (!isNaN(numValue)) { handleChange(leafKey, numValue); } } }} className="w-32" /> )} {field.type === 'time' && ( handleChange(leafKey, e.target.value)} className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" /> )} {field.type === 'select' && ( )} {field.type === 'post_types' && (
{postTypeOptions.map(opt => { const checked = Array.isArray(vals[leafKey]) && vals[leafKey].includes(opt.value); return (
{ setSettings(prev => { const safePrev = prev || {}; const current = Array.isArray(safePrev[leafKey]) ? safePrev[leafKey] : []; return { ...safePrev, [leafKey]: val ? [...current, opt.value] : current.filter((v: string) => v !== opt.value) }; }); }} id={`switch-${leafKey}-${opt.value}`} />
); })}
)} {field.description && (
{field.description}
)}
); } if (loading) return (
{/* Section 1 */} {/* Tabs */}
{[...Array(3)].map((_, i) => (
{/* Label */}
{i === 0 ? ( // Switch skeleton
) : i === 1 ? ( // Select skeleton ) : ( // Time input skeleton )} {/* Description */}
))}
{/* Divider */} {/* Section 2 */} {/* Section Title */}
{[...Array(4)].map((_, i) => (
{/* Label */}
{i === 0 ? ( // Post types switches
{[...Array(3)].map((_, j) => (
))}
) : ( // Switch skeleton
)} {/* Description */}
))}
{/* Save button */}
); if (!schema || !Array.isArray(schema.tabs) || !settings) return null; return (
{schema.tabs.map(tab => { let Icon = SettingsIcon; if (tab.key === 'api') { Icon = KeyIcon; } if (tab.key === 'schedule') { Icon = CalendarSyncIcon; } if (tab.key === 'mcp') { Icon = PlugIcon; } return ( {tab.label} ); })} {schema.tabs.map(tab => ( {tab.sections && tab.sections.map((section, sidx) => ( {section.title && (
{section.title}
)} {section.description && (
{section.description}
)} {section.fields && (
{section.fields.map((field, fidx) => renderSchemaField(field, `sec-${sidx}-f${fidx}`) )}
)} {(section.content && section.content.length > 0) && (
{section.content.map((item, i) => { if (item.type === 'endpoint') { return (
{item.method}
{item.url.startsWith('/') && window.worddown_variables?.siteUrl ? `${window.worddown_variables.siteUrl}${item.url}` : item.url}
{item.desc}
); } if (item.type === 'code') { return (
{interpolateCodeTemplate(item.code)}
); } return null; })}
)} {tab.sections && sidx < tab.sections.length - 1 && (
)}
))}
))}
); }