Files
WooNooW/admin-spa/src/lib/api.ts
Dwindi Ramadhana 396ca25be4 feat: Page Editor v1.0 - canonical schema, SSR parity, and migration
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
2026-05-30 13:02:08 +07:00

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) || [];
}
}