Major improvements to WooNooW Page Editor system: Schema & Architecture: - Canonical section schema with unified sectionSchema.ts - Normalized feature-grid to use items (not features) - Standardized default values across all section types - Schema versioning with automatic migration on read Backend (PHP): - Enhanced PlaceholderRenderer with typed output contracts - Added fallback behavior for empty/invalid dynamic sources - Added caching support for post data resolution - New SchemaMigration class for backward compatibility - New Features class for feature flags - Enhanced PageSSR with full style support - Removed controller-level special-casing for related_posts Frontend (Admin SPA): - Updated CanvasRenderer with schema-aware transformation - Enhanced InspectorPanel with canonical schema metadata - Added new section renderers Frontend (Customer SPA): - New section components: BentoCategoryGrid, MarqueeBanner, ProductCarousel, ShoppableImage - Updated FeatureGridSection for items prop contract Testing: - Add PHP tests: SchemaMigrationTest, PlaceholderRendererTest, PageSSRTest - Add TypeScript tests: schema-integration, feature-grid-regression - Add parity tests for React vs SSR content matching - Add CI script: check-schema-drift.mjs - Add VERIFICATION_CHECKLIST.md Documentation: - RELEASE_NOTES-v1.0.md with full release notes - docs/PAGE_EDITOR_SECTION_SCHEMA_V1.md - docs/PAGE_EDITOR_SSR_COVERAGE_AUDIT.md
85 lines
2.7 KiB
TypeScript
85 lines
2.7 KiB
TypeScript
export const api = {
|
|
root: () => (window.WNW_API?.root?.replace(/\/$/, '') || ''),
|
|
nonce: () => (window.WNW_API?.nonce || ''),
|
|
|
|
async wpFetch<T = any>(path: string, options: RequestInit = {}): Promise<T> {
|
|
const url = /^https?:\/\//.test(path) ? path : api.root() + path;
|
|
const headers = new Headers(options.headers || {});
|
|
if (!headers.has('X-WP-Nonce') && api.nonce()) headers.set('X-WP-Nonce', api.nonce());
|
|
if (!headers.has('Accept')) headers.set('Accept', 'application/json');
|
|
if (options.body && !headers.has('Content-Type')) headers.set('Content-Type', 'application/json');
|
|
|
|
const res = await fetch(url, { credentials: 'include', ...options, headers });
|
|
|
|
if (!res.ok) {
|
|
let responseData: any = null;
|
|
try {
|
|
const text = await res.text();
|
|
responseData = text ? JSON.parse(text) : null;
|
|
} catch { /* ignore JSON parse errors */ }
|
|
|
|
if (window.WNW_API?.isDev) {
|
|
console.error('[WooNooW] API error', { url, status: res.status, statusText: res.statusText, data: responseData });
|
|
}
|
|
|
|
// Create error with response data attached (for error handling utility to extract)
|
|
const err: any = new Error(res.statusText);
|
|
err.response = {
|
|
status: res.status,
|
|
statusText: res.statusText,
|
|
data: responseData
|
|
};
|
|
throw err;
|
|
}
|
|
|
|
try {
|
|
return await res.json() as T;
|
|
} catch {
|
|
return await res.text() as unknown as T;
|
|
}
|
|
},
|
|
|
|
async get<T = any>(path: string, params?: Record<string, any>): Promise<T> {
|
|
const usp = new URLSearchParams();
|
|
if (params) {
|
|
for (const [k, v] of Object.entries(params)) {
|
|
if (v == null) continue;
|
|
usp.set(k, String(v));
|
|
}
|
|
}
|
|
const qs = usp.toString();
|
|
return api.wpFetch<T>(path + (qs ? `?${qs}` : ''));
|
|
},
|
|
|
|
async post<T = any>(path: string, body?: any): Promise<T> {
|
|
return api.wpFetch<T>(path, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
});
|
|
},
|
|
|
|
async put<T = any>(path: string, body?: any): Promise<T> {
|
|
return api.wpFetch<T>(path, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: body != null ? JSON.stringify(body) : undefined,
|
|
});
|
|
},
|
|
|
|
async del<T = any>(path: string): Promise<T> {
|
|
return api.wpFetch<T>(path, { method: 'DELETE' });
|
|
},
|
|
|
|
};
|
|
|
|
export async function getMenus() {
|
|
// Prefer REST; fall back to localized snapshot
|
|
try {
|
|
const res = await fetch(`${window.WNW_API}/menus`, { credentials: 'include' });
|
|
if (!res.ok) throw new Error('menus fetch failed');
|
|
return (await res.json()).items || [];
|
|
} catch {
|
|
return (window.WNW_WC_MENUS?.items) || [];
|
|
}
|
|
} |