@inspirer-dev/hero-widget-selector
Version:
A custom field plugin for Strapi v5 that provides a widget selector with size filtering capabilities. Perfect for selecting hero widgets from a filtered collection based on size (S, M, L, XL).
328 lines (327 loc) • 10.2 kB
JavaScript
;
const bootstrap = ({ strapi }) => {
};
const destroy = ({ strapi }) => {
};
const register = ({ strapi }) => {
strapi.customFields.register({
name: "widget-selector",
plugin: "hero-widget-selector",
// This must match the pluginId in admin registration
type: "string"
});
};
const config = {
default: {},
validator() {
}
};
const contentTypes = {};
const controller = ({ strapi }) => ({
index(ctx) {
ctx.body = strapi.plugin("hero-widget-selector").service("service").getWelcomeMessage();
},
async getFilteredWidgets(ctx) {
const { size, sizes } = ctx.query;
try {
let entities;
if (size) {
const knex = strapi.db.connection;
const results = await knex("widgets").select("id", "document_id", "title", "sizes", "subtitle").whereRaw("sizes->>'sizes' LIKE ?", [`%"${size}"%`]);
entities = results.map((row) => ({
id: row.id,
documentId: row.document_id,
title: row.title,
subtitle: row.subtitle,
sizes: typeof row.sizes === "string" ? JSON.parse(row.sizes) : row.sizes
}));
const populatedEntities = await Promise.all(
entities.map(async (entity) => {
const fullEntity = await strapi.entityService.findOne("api::widget.widget", entity.id, {
populate: {
image: {
fields: ["id", "documentId", "name", "url", "alternativeText"]
},
ctaButton: true
}
});
return {
...entity,
image: fullEntity?.image,
ctaButton: fullEntity?.ctaButton
};
})
);
entities = populatedEntities;
} else {
entities = await strapi.entityService.findMany("api::widget.widget", {
fields: ["id", "documentId", "title", "sizes", "subtitle"],
populate: {
image: {
fields: ["id", "documentId", "name", "url", "alternativeText"]
},
ctaButton: true
},
pagination: {
pageSize: 100
}
});
}
ctx.body = {
results: entities.map((entity) => ({
id: entity.id,
documentId: entity.documentId,
title: entity.title,
subtitle: entity.subtitle,
sizes: entity.sizes,
image: entity.image,
ctaButton: entity.ctaButton
}))
};
} catch (err) {
strapi.log.error("Failed to fetch widgets:", err);
ctx.throw(500, `Failed to fetch widgets: ${err.message}`);
}
},
async getHeroLayouts(ctx) {
const { search, omitLayoutId, omitLayoutSlug, excludeIndex, fallbackOnly } = ctx.query;
try {
const heroSection = await strapi.entityService.findOne("api::hero-section.hero-section", 1, {
populate: {
defaultLayouts: {
populate: "*"
},
fallbackLayout: {
populate: "*"
}
}
});
if (!heroSection) {
ctx.body = { results: [] };
return;
}
let layouts = [];
if (fallbackOnly === "true") {
layouts = heroSection.fallbackLayout || [];
console.log("Fetching fallback layouts only:", layouts.length);
} else {
layouts = heroSection.defaultLayouts || [];
console.log("Fetching default layouts:", layouts.length);
if (heroSection.fallbackLayout && heroSection.fallbackLayout.length > 0) {
const fallbackComponents = new Set(
heroSection.fallbackLayout.map((layout) => layout.__component)
);
const fallbackSlugs = new Set(
heroSection.fallbackLayout.map((layout) => layout.slug).filter(Boolean)
);
layouts = layouts.filter((layout) => {
const isFallbackComponent = fallbackComponents.has(layout.__component);
const isFallbackSlug = layout.slug && fallbackSlugs.has(layout.slug);
return !isFallbackComponent && !isFallbackSlug;
});
console.log("Filtered out fallback layouts, remaining:", layouts.length);
}
}
if (omitLayoutId) {
const omitId = omitLayoutId.toString();
layouts = layouts.filter((layout) => {
const layoutIdStr = layout.id ? layout.id.toString() : "";
const documentIdStr = layout.documentId ? layout.documentId.toString() : "";
return layoutIdStr !== omitId && documentIdStr !== omitId;
});
console.log(`Filtering out layout ID: ${omitId}, remaining layouts:`, layouts.length);
}
if (omitLayoutSlug) {
layouts = layouts.filter((layout) => layout.slug !== omitLayoutSlug);
console.log(
`Filtering out layout slug: ${omitLayoutSlug}, remaining layouts:`,
layouts.length
);
}
if (!omitLayoutId && excludeIndex !== void 0) {
const excludeIdx = parseInt(excludeIndex);
layouts = layouts.filter((layout, index2) => index2 !== excludeIdx);
console.log(
`Filtering out layout at index: ${excludeIdx}, remaining layouts:`,
layouts.length
);
}
if (search && search.trim()) {
const searchTerm = search.toLowerCase().trim();
layouts = layouts.filter((layout) => {
const name = layout.name?.toLowerCase() || "";
const displayName = layout.__component?.split(".")[1]?.replace(/-/g, " ") || "";
return name.includes(searchTerm) || displayName.includes(searchTerm);
});
}
const results = layouts.map((layout) => {
const autoSlug = layout.slug || layout.name?.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "") || layout.__component?.split(".")[1] || `layout-${layout.id}`;
console.log("Layout data:", {
id: layout.id,
documentId: layout.documentId,
slug: layout.slug || autoSlug,
name: layout.name,
component: layout.__component,
isFallback: fallbackOnly === "true"
});
return {
id: layout.id,
documentId: layout.documentId,
slug: layout.slug || autoSlug,
// Use actual slug or fallback
name: layout.name || "",
displayName: layout.__component?.split(".")[1]?.replace(/-/g, " ").toUpperCase() || "Unknown Layout",
component: layout.__component,
__component: layout.__component,
isFallback: fallbackOnly === "true"
};
});
console.log("Returning layout results:", results);
ctx.body = { results };
} catch (err) {
strapi.log.error("Failed to fetch hero layouts:", err);
ctx.throw(500, `Failed to fetch hero layouts: ${err.message}`);
}
},
async getPopulatedLayout(ctx) {
const { layoutId, layoutSlug } = ctx.params;
try {
const heroSection = await strapi.entityService.findOne("api::hero-section.hero-section", 1, {
populate: {
defaultLayouts: {
populate: {
extraLarge: true,
small1: true,
small2: true,
medium: true,
large1: true,
large2: true
}
}
}
});
if (!heroSection || !heroSection.defaultLayouts) {
ctx.throw(404, "Hero section not found");
}
let layout;
if (layoutSlug) {
layout = heroSection.defaultLayouts.find((l) => l.slug === layoutSlug);
} else if (layoutId) {
layout = heroSection.defaultLayouts.find((l) => l.id.toString() === layoutId);
}
if (!layout) {
ctx.throw(404, "Layout not found");
}
const populatedLayout = {
id: layout.id,
name: layout.name,
component: layout.__component,
displayName: layout.__component?.split(".")[1]?.replace(/-/g, " ").toUpperCase() || "Unknown Layout",
widgets: {}
};
const positions = ["extraLarge", "small1", "small2", "medium", "large1", "large2"];
for (const position of positions) {
if (layout[position]) {
let widgetData = layout[position];
if (typeof widgetData === "string") {
try {
widgetData = JSON.parse(widgetData);
} catch (e) {
widgetData = { widgetId: widgetData };
}
}
populatedLayout.widgets[position] = widgetData;
}
}
ctx.body = { layout: populatedLayout };
} catch (err) {
strapi.log.error("Failed to fetch populated layout:", err);
ctx.throw(500, `Failed to fetch populated layout: ${err.message}`);
}
}
});
const controllers = {
controller
};
const middlewares = {};
const policies = {};
const contentAPIRoutes = [
{
method: "GET",
path: "/",
// name of the controller file & the method.
handler: "controller.index",
config: {
policies: []
}
}
];
const adminAPIRoutes = [
{
method: "GET",
path: "/widgets/admin",
handler: "controller.getFilteredWidgets",
config: {
policies: [],
auth: false
}
},
{
method: "GET",
path: "/hero-layouts/admin",
handler: "controller.getHeroLayouts",
config: {
policies: [],
auth: false
}
},
{
method: "GET",
path: "/hero-layouts/:layoutId/populated",
handler: "controller.getPopulatedLayout",
config: {
policies: [],
auth: false
}
},
{
method: "GET",
path: "/hero-layouts/slug/:layoutSlug/populated",
handler: "controller.getPopulatedLayout",
config: {
policies: [],
auth: false
}
}
];
const routes = {
"content-api": {
type: "content-api",
routes: contentAPIRoutes
},
admin: {
type: "admin",
routes: adminAPIRoutes
}
};
const service = ({ strapi }) => ({
getWelcomeMessage() {
return "Welcome to Strapi 🚀";
}
});
const services = {
service
};
const index = {
register,
bootstrap,
destroy,
config,
controllers,
routes,
services,
contentTypes,
policies,
middlewares
};
module.exports = index;