ifixkart-storefront/lib/layoutConfig.ts

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;
}