first commit
This commit is contained in:
105
packages/core/src/widgets/components.ts
Normal file
105
packages/core/src/widgets/components.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import type { WidgetComponentDef } from "./types.js";
|
||||
|
||||
/**
|
||||
* Core widget components registry
|
||||
* These are built-in widgets that ship with EmDash
|
||||
*/
|
||||
export const coreWidgetComponents: WidgetComponentDef[] = [
|
||||
{
|
||||
id: "core:recent-posts",
|
||||
label: "Recent Posts",
|
||||
description: "Display a list of recent posts",
|
||||
props: {
|
||||
count: {
|
||||
type: "number",
|
||||
label: "Number of posts",
|
||||
default: 5,
|
||||
},
|
||||
showThumbnails: {
|
||||
type: "boolean",
|
||||
label: "Show thumbnails",
|
||||
default: false,
|
||||
},
|
||||
showDate: {
|
||||
type: "boolean",
|
||||
label: "Show date",
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "core:categories",
|
||||
label: "Categories",
|
||||
description: "Display category list",
|
||||
props: {
|
||||
showCount: {
|
||||
type: "boolean",
|
||||
label: "Show post count",
|
||||
default: true,
|
||||
},
|
||||
hierarchical: {
|
||||
type: "boolean",
|
||||
label: "Show hierarchy",
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "core:tags",
|
||||
label: "Tags",
|
||||
description: "Display tag cloud",
|
||||
props: {
|
||||
showCount: {
|
||||
type: "boolean",
|
||||
label: "Show count",
|
||||
default: false,
|
||||
},
|
||||
limit: {
|
||||
type: "number",
|
||||
label: "Maximum tags",
|
||||
default: 20,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "core:search",
|
||||
label: "Search",
|
||||
description: "Search form",
|
||||
props: {
|
||||
placeholder: {
|
||||
type: "string",
|
||||
label: "Placeholder text",
|
||||
default: "Search...",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "core:archives",
|
||||
label: "Archives",
|
||||
description: "Monthly/yearly archives",
|
||||
props: {
|
||||
type: {
|
||||
type: "select",
|
||||
label: "Group by",
|
||||
default: "monthly",
|
||||
options: [
|
||||
{ value: "monthly", label: "Monthly" },
|
||||
{ value: "yearly", label: "Yearly" },
|
||||
],
|
||||
},
|
||||
limit: {
|
||||
type: "number",
|
||||
label: "Limit",
|
||||
default: 12,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Get all widget component definitions (core + plugin-registered)
|
||||
* For now, only returns core components. Plugin widgets will be added later.
|
||||
*/
|
||||
export function getWidgetComponents(): WidgetComponentDef[] {
|
||||
return [...coreWidgetComponents];
|
||||
}
|
||||
131
packages/core/src/widgets/index.ts
Normal file
131
packages/core/src/widgets/index.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { getDb } from "../loader.js";
|
||||
import { getWidgetComponents as getComponentRegistry } from "./components.js";
|
||||
import type { Widget, WidgetArea, WidgetRow, WidgetComponentDef } from "./types.js";
|
||||
|
||||
export type {
|
||||
Widget,
|
||||
WidgetArea,
|
||||
WidgetType,
|
||||
WidgetComponentDef,
|
||||
PropDef,
|
||||
CreateWidgetAreaInput,
|
||||
CreateWidgetInput,
|
||||
UpdateWidgetInput,
|
||||
ReorderWidgetsInput,
|
||||
} from "./types.js";
|
||||
|
||||
/**
|
||||
* Get a widget area by name, with all its widgets
|
||||
*/
|
||||
export async function getWidgetArea(name: string): Promise<WidgetArea | null> {
|
||||
const db = await getDb();
|
||||
// Get the area
|
||||
const areaRow = await db
|
||||
.selectFrom("_emdash_widget_areas")
|
||||
.selectAll()
|
||||
.where("name", "=", name)
|
||||
.executeTakeFirst();
|
||||
|
||||
if (!areaRow) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get widgets for this area, ordered by sort_order
|
||||
const widgetRows = await db
|
||||
.selectFrom("_emdash_widgets")
|
||||
.selectAll()
|
||||
.$castTo<WidgetRow>()
|
||||
.where("area_id", "=", areaRow.id)
|
||||
.orderBy("sort_order", "asc")
|
||||
.execute();
|
||||
|
||||
// Map to API types
|
||||
const widgets: Widget[] = widgetRows.map((row) => rowToWidget(row));
|
||||
|
||||
return {
|
||||
id: areaRow.id,
|
||||
name: areaRow.name,
|
||||
label: areaRow.label,
|
||||
description: areaRow.description ?? undefined,
|
||||
widgets,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all widget areas with their widgets
|
||||
*/
|
||||
export async function getWidgetAreas(): Promise<WidgetArea[]> {
|
||||
const db = await getDb();
|
||||
// Get all areas
|
||||
const areaRows = await db.selectFrom("_emdash_widget_areas").selectAll().execute();
|
||||
|
||||
// Get all widgets
|
||||
const widgetRows = await db
|
||||
.selectFrom("_emdash_widgets")
|
||||
.selectAll()
|
||||
.$castTo<WidgetRow>()
|
||||
.orderBy("sort_order", "asc")
|
||||
.execute();
|
||||
|
||||
// Group widgets by area
|
||||
const widgetsByArea = new Map<string, Widget[]>();
|
||||
for (const row of widgetRows) {
|
||||
if (!widgetsByArea.has(row.area_id)) {
|
||||
widgetsByArea.set(row.area_id, []);
|
||||
}
|
||||
widgetsByArea.get(row.area_id)!.push(rowToWidget(row));
|
||||
}
|
||||
|
||||
// Combine
|
||||
return areaRows.map((areaRow) => ({
|
||||
id: areaRow.id,
|
||||
name: areaRow.name,
|
||||
label: areaRow.label,
|
||||
description: areaRow.description ?? undefined,
|
||||
widgets: widgetsByArea.get(areaRow.id) || [],
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available widget components (for admin UI)
|
||||
*/
|
||||
export function getWidgetComponents(): WidgetComponentDef[] {
|
||||
return getComponentRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a widget row to the API type
|
||||
*/
|
||||
function rowToWidget(row: WidgetRow): Widget {
|
||||
const widget: Widget = {
|
||||
id: row.id,
|
||||
type: row.type,
|
||||
title: row.title ?? undefined,
|
||||
};
|
||||
|
||||
// Type-specific fields
|
||||
if (row.type === "content" && row.content) {
|
||||
try {
|
||||
widget.content = JSON.parse(row.content);
|
||||
} catch {
|
||||
// Invalid JSON, ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (row.type === "menu" && row.menu_name) {
|
||||
widget.menuName = row.menu_name;
|
||||
}
|
||||
|
||||
if (row.type === "component" && row.component_id) {
|
||||
widget.componentId = row.component_id;
|
||||
if (row.component_props) {
|
||||
try {
|
||||
widget.componentProps = JSON.parse(row.component_props);
|
||||
} catch {
|
||||
// Invalid JSON, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return widget;
|
||||
}
|
||||
81
packages/core/src/widgets/types.ts
Normal file
81
packages/core/src/widgets/types.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { PortableTextBlock } from "../fields/index.js";
|
||||
|
||||
export type WidgetType = "content" | "menu" | "component";
|
||||
|
||||
export interface Widget {
|
||||
id: string;
|
||||
type: WidgetType;
|
||||
title?: string;
|
||||
// Type-specific fields
|
||||
content?: PortableTextBlock[]; // For content type
|
||||
menuName?: string; // For menu type
|
||||
componentId?: string; // For component type
|
||||
componentProps?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface WidgetArea {
|
||||
id: string;
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
widgets: Widget[];
|
||||
}
|
||||
|
||||
// For DB layer
|
||||
export interface WidgetRow {
|
||||
id: string;
|
||||
area_id: string;
|
||||
sort_order: number;
|
||||
type: WidgetType;
|
||||
title: string | null;
|
||||
content: string | null; // JSON string
|
||||
menu_name: string | null;
|
||||
component_id: string | null;
|
||||
component_props: string | null; // JSON string
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface WidgetAreaRow {
|
||||
id: string;
|
||||
name: string;
|
||||
label: string;
|
||||
description: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Component registration
|
||||
export interface WidgetComponentDef {
|
||||
id: string; // 'core:recent-posts'
|
||||
label: string; // 'Recent Posts'
|
||||
description?: string;
|
||||
props: Record<string, PropDef>;
|
||||
}
|
||||
|
||||
export interface PropDef {
|
||||
type: "string" | "number" | "boolean" | "select";
|
||||
label: string;
|
||||
default?: unknown;
|
||||
options?: Array<{ value: string; label: string }>; // For select
|
||||
}
|
||||
|
||||
// Admin API types
|
||||
export interface CreateWidgetAreaInput {
|
||||
name: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface CreateWidgetInput {
|
||||
type: WidgetType;
|
||||
title?: string;
|
||||
content?: PortableTextBlock[];
|
||||
menuName?: string;
|
||||
componentId?: string;
|
||||
componentProps?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface UpdateWidgetInput extends Partial<CreateWidgetInput> {}
|
||||
|
||||
export interface ReorderWidgetsInput {
|
||||
widgetIds: string[];
|
||||
}
|
||||
Reference in New Issue
Block a user