2026-04-05 18:31:16 -07:00
|
|
|
import { useCallback } from 'react';
|
|
|
|
|
import { useEditor } from '@craftjs/core';
|
|
|
|
|
import { useEditorConfig } from '../state/EditorConfigContext';
|
|
|
|
|
import { usePages } from '../state/PageContext';
|
|
|
|
|
import { exportBodyHtml } from '../utils/html-export';
|
|
|
|
|
|
|
|
|
|
export function useWhpApi() {
|
|
|
|
|
const { query, actions } = useEditor();
|
|
|
|
|
const { whpConfig, isWHP } = useEditorConfig();
|
|
|
|
|
const { pages, headerPage, footerPage, activePageId, setHeaderCraftState, setFooterCraftState, setPagesCraftState } = usePages();
|
|
|
|
|
|
|
|
|
|
const save = useCallback(async () => {
|
|
|
|
|
if (!isWHP || !whpConfig) return null;
|
|
|
|
|
|
|
|
|
|
// Serialize the current canvas state (whatever page is active)
|
|
|
|
|
const currentCraftState = query.serialize();
|
|
|
|
|
|
|
|
|
|
// Export body HTML for the current page
|
|
|
|
|
let currentHtml = '';
|
|
|
|
|
let css = '';
|
|
|
|
|
try {
|
|
|
|
|
const result = exportBodyHtml(currentCraftState);
|
|
|
|
|
currentHtml = result.html;
|
|
|
|
|
css = result.css;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('HTML export failed, saving state only:', e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Export header HTML from its craft state
|
|
|
|
|
let headerHtml = '';
|
|
|
|
|
try {
|
|
|
|
|
if (headerPage.craftState) {
|
|
|
|
|
const hResult = exportBodyHtml(headerPage.craftState);
|
|
|
|
|
headerHtml = hResult.html;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Header HTML export failed:', e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Export footer HTML from its craft state
|
|
|
|
|
let footerHtml = '';
|
|
|
|
|
try {
|
|
|
|
|
if (footerPage.craftState) {
|
|
|
|
|
const fResult = exportBodyHtml(footerPage.craftState);
|
|
|
|
|
footerHtml = fResult.html;
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('Footer HTML export failed:', e);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Build the pages array with HTML for each page
|
|
|
|
|
// For the active page, use the freshly exported HTML from the canvas;
|
|
|
|
|
// for others, export from their stored craft state
|
2026-05-25 12:14:26 -07:00
|
|
|
const pagesPayload = pages.map((page, i) => {
|
|
|
|
|
// The first page is ALWAYS the landing page → publishes to index.html
|
|
|
|
|
// regardless of the page name/slug. Apache serves '/' from index.html,
|
|
|
|
|
// and renaming the first page should not break the root URL.
|
|
|
|
|
const filename = i === 0 ? 'index.html' : page.slug + '.html';
|
2026-04-05 18:31:16 -07:00
|
|
|
let pageHtml = '';
|
|
|
|
|
|
|
|
|
|
if (page.id === activePageId) {
|
|
|
|
|
// Active page: use the current canvas HTML (already exported above)
|
|
|
|
|
pageHtml = currentHtml;
|
|
|
|
|
} else if (page.craftState) {
|
|
|
|
|
try {
|
|
|
|
|
const pResult = exportBodyHtml(page.craftState);
|
|
|
|
|
pageHtml = pResult.html;
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error(`HTML export failed for page ${page.name}:`, e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
filename,
|
|
|
|
|
title: page.name,
|
|
|
|
|
html: pageHtml,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Build pages_craft_state array: for each page, store its craft state
|
|
|
|
|
// For the currently active page, always use the fresh canvas state (currentCraftState)
|
|
|
|
|
// since page.craftState may be stale (not updated until page switch)
|
2026-05-25 12:14:26 -07:00
|
|
|
const pagesGrapesjs = pages.map((page, i) => ({
|
2026-04-05 18:31:16 -07:00
|
|
|
id: page.id,
|
|
|
|
|
name: page.name,
|
2026-05-25 12:14:26 -07:00
|
|
|
// Pin the landing page's slug to 'index' on the wire too, so that on
|
|
|
|
|
// reload the editor's clean-URL routing (.htaccess rewrite of /name →
|
|
|
|
|
// name.html) lines up with the file we just wrote (index.html).
|
|
|
|
|
slug: i === 0 ? 'index' : page.slug,
|
2026-04-05 18:31:16 -07:00
|
|
|
craftState: page.id === activePageId ? currentCraftState : (page.craftState || null),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
const payload = {
|
|
|
|
|
site_id: whpConfig.siteId,
|
|
|
|
|
name: whpConfig.siteName,
|
|
|
|
|
html: currentHtml,
|
|
|
|
|
css,
|
|
|
|
|
pages: pagesPayload,
|
|
|
|
|
header_html: headerHtml,
|
|
|
|
|
footer_html: footerHtml,
|
|
|
|
|
craft_state: currentCraftState,
|
|
|
|
|
header_craft_state: headerPage.craftState || null,
|
|
|
|
|
footer_craft_state: footerPage.craftState || null,
|
|
|
|
|
pages_craft_state: pagesGrapesjs,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const resp = await fetch(`${whpConfig.apiUrl}?action=save`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'X-CSRF-Token': whpConfig.csrfToken,
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(payload),
|
|
|
|
|
});
|
|
|
|
|
return resp.json();
|
|
|
|
|
}, [isWHP, whpConfig, query, pages, activePageId, headerPage, footerPage]);
|
|
|
|
|
|
|
|
|
|
const publish = useCallback(async () => {
|
|
|
|
|
if (!isWHP || !whpConfig) return null;
|
|
|
|
|
|
|
|
|
|
// First save to ensure staging is up to date
|
|
|
|
|
await save();
|
|
|
|
|
|
|
|
|
|
// Then publish from staging to live
|
|
|
|
|
const resp = await fetch(
|
|
|
|
|
`${whpConfig.apiUrl}?action=publish&site_id=${whpConfig.siteId}`,
|
|
|
|
|
{
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
'X-CSRF-Token': whpConfig.csrfToken,
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({ site_id: whpConfig.siteId }),
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return resp.json();
|
|
|
|
|
}, [isWHP, whpConfig, save]);
|
|
|
|
|
|
|
|
|
|
const load = useCallback(async () => {
|
|
|
|
|
if (!isWHP || !whpConfig) return null;
|
|
|
|
|
|
|
|
|
|
const resp = await fetch(
|
|
|
|
|
`${whpConfig.apiUrl}?action=load&site_id=${whpConfig.siteId}`,
|
|
|
|
|
);
|
|
|
|
|
const data = await resp.json();
|
|
|
|
|
|
|
|
|
|
if (data.success && data.project) {
|
|
|
|
|
const proj = data.project;
|
|
|
|
|
|
|
|
|
|
// Restore header craft state
|
|
|
|
|
if (proj.header_craft_state) {
|
|
|
|
|
setHeaderCraftState(typeof proj.header_craft_state === 'string'
|
|
|
|
|
? proj.header_craft_state : JSON.stringify(proj.header_craft_state));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Restore footer craft state
|
|
|
|
|
if (proj.footer_craft_state) {
|
|
|
|
|
setFooterCraftState(typeof proj.footer_craft_state === 'string'
|
|
|
|
|
? proj.footer_craft_state : JSON.stringify(proj.footer_craft_state));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Restore pages and load the first page into the canvas
|
|
|
|
|
if (proj.pages_craft_state && Array.isArray(proj.pages_craft_state) && proj.pages_craft_state.length > 0) {
|
|
|
|
|
setPagesCraftState(proj.pages_craft_state.map((p: { id: string; name: string; slug: string; craftState: string | null }) => ({
|
|
|
|
|
id: p.id, name: p.name, slug: p.slug, craftState: p.craftState || null,
|
|
|
|
|
})));
|
|
|
|
|
|
|
|
|
|
// Load the first page (home) into the canvas
|
|
|
|
|
const firstPage = proj.pages_craft_state[0];
|
|
|
|
|
if (firstPage.craftState) {
|
|
|
|
|
try {
|
|
|
|
|
const state = typeof firstPage.craftState === 'string'
|
|
|
|
|
? firstPage.craftState : JSON.stringify(firstPage.craftState);
|
|
|
|
|
actions.deserialize(state);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn('Failed to load page state:', e);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
}, [isWHP, whpConfig, actions, setHeaderCraftState, setFooterCraftState, setPagesCraftState]);
|
|
|
|
|
|
|
|
|
|
const uploadAsset = useCallback(
|
|
|
|
|
async (file: File) => {
|
|
|
|
|
if (!isWHP || !whpConfig) return null;
|
|
|
|
|
|
|
|
|
|
const formData = new FormData();
|
|
|
|
|
formData.append('file', file);
|
|
|
|
|
|
|
|
|
|
const resp = await fetch(
|
|
|
|
|
`${whpConfig.apiUrl}?action=upload_asset&site_id=${whpConfig.siteId}`,
|
|
|
|
|
{
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'X-CSRF-Token': whpConfig.csrfToken },
|
|
|
|
|
body: formData,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return resp.json();
|
|
|
|
|
},
|
|
|
|
|
[isWHP, whpConfig],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return { save, publish, load, uploadAsset, isWHP };
|
|
|
|
|
}
|