diff --git a/src/app/(main)/boards/TextBlock.tsx b/src/app/(main)/boards/TextBlock.tsx
new file mode 100644
index 000000000..6e199ec5e
--- /dev/null
+++ b/src/app/(main)/boards/TextBlock.tsx
@@ -0,0 +1,10 @@
+'use client';
+import { Text } from '@umami/react-zen';
+
+export function TextBlock({ text }: { text?: string }) {
+ if (!text) {
+ return null;
+ }
+
+ return {text};
+}
diff --git a/src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx b/src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx
index 3609e9b9e..f6449fbd4 100644
--- a/src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx
+++ b/src/app/(main)/boards/[boardId]/BoardComponentRenderer.tsx
@@ -22,7 +22,7 @@ function BoardComponentRendererComponent({
const Component = definition.component;
- if (!websiteId) {
+ if (!websiteId && definition.requiresWebsite !== false) {
return (
Select a website
@@ -30,7 +30,7 @@ function BoardComponentRendererComponent({
);
}
- return ;
+ return ;
}
export const BoardComponentRenderer = memo(
diff --git a/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx b/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx
index c88714f04..e3111b191 100644
--- a/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx
+++ b/src/app/(main)/boards/[boardId]/BoardComponentSelect.tsx
@@ -97,8 +97,10 @@ export function BoardComponentSelect({
setConfigValues(prev => ({ ...prev, [name]: value }));
};
+ const needsWebsite = selectedDef?.requiresWebsite !== false;
+
const handleAdd = () => {
- if (!selectedDef || !selectedWebsiteId) return;
+ if (!selectedDef || (needsWebsite && !selectedWebsiteId)) return;
const props: Record = {};
@@ -116,7 +118,7 @@ export function BoardComponentSelect({
const config: BoardComponentConfig = {
type: selectedDef.type,
- websiteId: selectedWebsiteId,
+ ...(needsWebsite ? { websiteId: selectedWebsiteId } : {}),
title,
description,
};
@@ -137,7 +139,7 @@ export function BoardComponentSelect({
}
: null;
- const canSave = !!selectedDef && !!selectedWebsiteId;
+ const canSave = !!selectedDef && (!needsWebsite || !!selectedWebsiteId);
return (
@@ -184,7 +186,7 @@ export function BoardComponentSelect({
- {previewConfig && selectedWebsiteId ? (
+ {previewConfig && (!needsWebsite || selectedWebsiteId) ? (
) : (
@@ -201,17 +203,19 @@ export function BoardComponentSelect({
{t(labels.properties)}
-
-
- {t(labels.website)}
-
-
-
+ {needsWebsite && (
+
+
+ {t(labels.website)}
+
+
+
+ )}
@@ -262,6 +266,15 @@ export function BoardComponentSelect({
onChange={(value: string) => handleConfigChange(field.name, value)}
/>
)}
+
+ {field.type === 'textarea' && (
+ handleConfigChange(field.name, value)}
+ style={{ minHeight: 200 }}
+ />
+ )}
))}
diff --git a/src/app/(main)/boards/[boardId]/BoardViewColumn.tsx b/src/app/(main)/boards/[boardId]/BoardViewColumn.tsx
index 4a5909eda..65df4fdca 100644
--- a/src/app/(main)/boards/[boardId]/BoardViewColumn.tsx
+++ b/src/app/(main)/boards/[boardId]/BoardViewColumn.tsx
@@ -2,13 +2,15 @@ import { Box, Column } from '@umami/react-zen';
import { Panel } from '@/components/common/Panel';
import { useBoard } from '@/components/hooks';
import type { BoardComponentConfig } from '@/lib/types';
+import { getComponentDefinition } from '../boardComponentRegistry';
import { BoardComponentRenderer } from './BoardComponentRenderer';
export function BoardViewColumn({ component }: { component?: BoardComponentConfig }) {
const { board } = useBoard();
+ const definition = component ? getComponentDefinition(component.type) : undefined;
const websiteId = component?.websiteId || board?.parameters?.websiteId;
- if (!component || !websiteId) {
+ if (!component || (!websiteId && definition?.requiresWebsite !== false)) {
return null;
}
diff --git a/src/app/(main)/boards/boardComponentRegistry.tsx b/src/app/(main)/boards/boardComponentRegistry.tsx
index 82843cca9..1d7222c60 100644
--- a/src/app/(main)/boards/boardComponentRegistry.tsx
+++ b/src/app/(main)/boards/boardComponentRegistry.tsx
@@ -1,4 +1,5 @@
import type { ComponentType } from 'react';
+import { TextBlock } from '@/app/(main)/boards/TextBlock';
import { WebsiteChart } from '@/app/(main)/websites/[websiteId]/WebsiteChart';
import { WebsiteMetricsBar } from '@/app/(main)/websites/[websiteId]/WebsiteMetricsBar';
import { EventsChart } from '@/components/metrics/EventsChart';
@@ -9,7 +10,7 @@ import { WorldMap } from '@/components/metrics/WorldMap';
export interface ConfigField {
name: string;
label: string;
- type: 'select' | 'number' | 'text';
+ type: 'select' | 'number' | 'text' | 'textarea';
options?: { label: string; value: string }[];
defaultValue?: any;
}
@@ -22,12 +23,14 @@ export interface ComponentDefinition {
component: ComponentType;
defaultProps?: Record;
configFields?: ConfigField[];
+ requiresWebsite?: boolean;
}
export const CATEGORIES = [
{ key: 'overview', name: 'Overview' },
{ key: 'tables', name: 'Tables' },
{ key: 'visualization', name: 'Visualization' },
+ { key: 'content', name: 'Content' },
] as const;
const METRIC_TYPES = [
@@ -121,6 +124,25 @@ const componentDefinitions: ComponentDefinition[] = [
category: 'visualization',
component: EventsChart,
},
+
+ // Content
+ {
+ type: 'TextBlock',
+ name: 'Text',
+ description: 'Free-form text content',
+ category: 'content',
+ component: TextBlock,
+ requiresWebsite: false,
+ defaultProps: { text: '' },
+ configFields: [
+ {
+ name: 'text',
+ label: 'Text',
+ type: 'textarea',
+ defaultValue: '',
+ },
+ ],
+ },
];
const definitionMap = new Map(componentDefinitions.map(def => [def.type, def]));