180 lines
5.5 KiB
TypeScript
180 lines
5.5 KiB
TypeScript
/**
|
|
* Shared helpers for storefront layout CMS payloads.
|
|
* Public API returns `metadata` (object); admin saves `metadata_json`.
|
|
*/
|
|
|
|
import {
|
|
DEFAULT_SECTION_ACTIVE,
|
|
DEFAULT_SECTION_TITLES,
|
|
HOMEPAGE_SECTION_KEYS,
|
|
} from './homepageDefaults';
|
|
|
|
export type LayoutItem = {
|
|
content_id?: string;
|
|
region?: string;
|
|
type?: string;
|
|
page?: string;
|
|
title?: string;
|
|
subtitle?: string;
|
|
display_order?: number;
|
|
is_active?: boolean | string;
|
|
metadata?: any;
|
|
metadata_json?: any;
|
|
[key: string]: any;
|
|
};
|
|
|
|
function asObject(raw: unknown): Record<string, any> {
|
|
if (!raw) return {};
|
|
if (typeof raw === 'string') {
|
|
try {
|
|
const parsed = JSON.parse(raw);
|
|
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
} catch {
|
|
return {};
|
|
}
|
|
}
|
|
return typeof raw === 'object' ? (raw as Record<string, any>) : {};
|
|
}
|
|
|
|
/** Normalize layout/home (or admin content/all) payload into an array. */
|
|
export function normalizeLayoutPayload(value: unknown): LayoutItem[] {
|
|
if (Array.isArray(value)) return value as LayoutItem[];
|
|
if (!value || typeof value !== 'object') return [];
|
|
const obj = value as Record<string, unknown>;
|
|
for (const key of ['items', 'data', 'results', 'sections', 'content']) {
|
|
if (Array.isArray(obj[key])) return obj[key] as LayoutItem[];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
export function parseMetadata(item: LayoutItem | null | undefined): Record<string, any> {
|
|
if (!item) return {};
|
|
const fromJson = asObject(item.metadata_json);
|
|
const fromMeta = asObject(item.metadata);
|
|
// Admin writes metadata_json; public API mirrors into metadata — prefer json when both set
|
|
const merged = { ...fromMeta, ...fromJson };
|
|
if (typeof fromJson.is_active === 'boolean') {
|
|
merged.is_active = fromJson.is_active;
|
|
} else if (typeof fromMeta.is_active === 'boolean') {
|
|
merged.is_active = fromMeta.is_active;
|
|
}
|
|
return merged;
|
|
}
|
|
|
|
/**
|
|
* Resolve Visible/Hidden from CMS.
|
|
* Explicit `false` always wins (hide). Unspecified defaults to visible.
|
|
*/
|
|
export function resolveIsActive(item: LayoutItem | null | undefined): boolean {
|
|
if (!item) return false;
|
|
const meta = parseMetadata(item);
|
|
const candidates = [
|
|
meta.is_active,
|
|
asObject(item.metadata_json).is_active,
|
|
asObject(item.metadata).is_active,
|
|
item.is_active,
|
|
];
|
|
|
|
// Prefer any explicit false first so hide is never missed when sources diverge
|
|
for (const v of candidates) {
|
|
if (v === false || v === 'false' || v === 0 || v === '0') return false;
|
|
}
|
|
for (const v of candidates) {
|
|
if (v === true || v === 'true' || v === 1 || v === '1') return true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function normalizeLayoutItem(item: LayoutItem): LayoutItem {
|
|
const metadata = parseMetadata(item);
|
|
return {
|
|
...item,
|
|
metadata,
|
|
metadata_json: metadata,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Build active flags + section payloads from layout/home response.
|
|
* Only `page: home` rows drive the storefront homepage.
|
|
*/
|
|
export function buildHomepageSections(layoutData: LayoutItem[] | unknown): {
|
|
activeSections: Record<string, boolean>;
|
|
sectionsData: Record<string, LayoutItem>;
|
|
layoutLoaded: boolean;
|
|
} {
|
|
const activeSections: Record<string, boolean> = {};
|
|
const sectionsData: Record<string, LayoutItem> = {};
|
|
const raw = normalizeLayoutPayload(layoutData);
|
|
const homeRows = raw.filter((item) => {
|
|
const page = String(item.page || 'home').toLowerCase();
|
|
return page === 'home';
|
|
});
|
|
const layoutLoaded = homeRows.length > 0;
|
|
|
|
if (!layoutLoaded) {
|
|
for (const key of HOMEPAGE_SECTION_KEYS) {
|
|
activeSections[key] = true;
|
|
const titles = DEFAULT_SECTION_TITLES[key];
|
|
sectionsData[key] = {
|
|
region: key,
|
|
type: key,
|
|
page: 'home',
|
|
title: titles?.title || key,
|
|
subtitle: titles?.subtitle || '',
|
|
metadata: { section_key: key, is_active: true },
|
|
metadata_json: { section_key: key, is_active: true },
|
|
};
|
|
}
|
|
return { activeSections, sectionsData, layoutLoaded: false };
|
|
}
|
|
|
|
const groups: Record<string, LayoutItem[]> = {};
|
|
for (const item of homeRows) {
|
|
const meta = parseMetadata(item);
|
|
const key = item.region || meta.section_key || item.type;
|
|
if (!key) continue;
|
|
if (!groups[key]) groups[key] = [];
|
|
groups[key].push(item);
|
|
}
|
|
|
|
for (const key of Object.keys(groups)) {
|
|
const items = [...groups[key]].sort(
|
|
(a, b) => (a.display_order || 0) - (b.display_order || 0)
|
|
);
|
|
// Use the latest row for this region (do not skip inactive — inactivity must apply)
|
|
const chosen = normalizeLayoutItem(items[items.length - 1] || items[0]);
|
|
activeSections[key] = resolveIsActive(chosen);
|
|
sectionsData[key] = chosen;
|
|
}
|
|
|
|
// Ensure every homepage key exists so Visible sections never vanish from a missing row
|
|
for (const key of HOMEPAGE_SECTION_KEYS) {
|
|
if (sectionsData[key]) continue;
|
|
const titles = DEFAULT_SECTION_TITLES[key];
|
|
activeSections[key] = DEFAULT_SECTION_ACTIVE[key] !== false;
|
|
sectionsData[key] = {
|
|
region: key,
|
|
type: key,
|
|
page: 'home',
|
|
title: titles?.title || key,
|
|
subtitle: titles?.subtitle || '',
|
|
metadata: { section_key: key, is_active: true },
|
|
metadata_json: { section_key: key, is_active: true },
|
|
};
|
|
}
|
|
|
|
return { activeSections, sectionsData, layoutLoaded: true };
|
|
}
|
|
|
|
/** True when section should render on the storefront. */
|
|
export function isSectionVisible(
|
|
activeSections: Record<string, boolean>,
|
|
key: string,
|
|
layoutLoaded: boolean
|
|
): boolean {
|
|
if (!layoutLoaded) {
|
|
return DEFAULT_SECTION_ACTIVE[key] !== false;
|
|
}
|
|
return activeSections[key] === true;
|
|
}
|