@startsimpli/ui 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/components/ActivityTimeline.tsx +173 -0
- package/src/components/LogActivityDialog.tsx +303 -0
- package/src/components/QuickLogButtons.tsx +32 -0
- package/src/components/badge/StageBadge.tsx +31 -0
- package/src/components/badge/index.ts +3 -0
- package/src/components/command-palette/CommandPalette.tsx +344 -0
- package/src/components/command-palette/command-palette-context.tsx +51 -0
- package/src/components/command-palette/index.ts +3 -0
- package/src/components/compose/compose-header.tsx +72 -0
- package/src/components/compose/compose-loading.tsx +13 -0
- package/src/components/compose/index.ts +6 -0
- package/src/components/compose/save-status-indicator.tsx +57 -0
- package/src/components/compose/send-confirmation-dialog.tsx +87 -0
- package/src/components/compose/subject-input.tsx +25 -0
- package/src/components/compose/useAutoSave.ts +93 -0
- package/src/components/dashboard/DashboardGrid.tsx +32 -0
- package/src/components/dashboard/DashboardSection.tsx +32 -0
- package/src/components/dashboard/MetricCard.tsx +129 -0
- package/src/components/dashboard/PeriodSelector.tsx +55 -0
- package/src/components/dashboard/SparklineTrend.tsx +102 -0
- package/src/components/dashboard/index.ts +14 -0
- package/src/components/email-dialogs/index.ts +14 -0
- package/src/components/email-dialogs/merge-fields.tsx +196 -0
- package/src/components/email-dialogs/preview-dialog.tsx +194 -0
- package/src/components/email-dialogs/schedule-dialog.tsx +297 -0
- package/src/components/email-dialogs/template-picker.tsx +225 -0
- package/src/components/email-dialogs/test-send-dialog.tsx +188 -0
- package/src/components/email-editor/add-block-menu.tsx +151 -0
- package/src/components/email-editor/block-toolbar.tsx +73 -0
- package/src/components/email-editor/blocks/button-block.tsx +44 -0
- package/src/components/email-editor/blocks/divider-block.tsx +43 -0
- package/src/components/email-editor/blocks/footer-block.tsx +39 -0
- package/src/components/email-editor/blocks/header-block.tsx +39 -0
- package/src/components/email-editor/blocks/image-block.tsx +61 -0
- package/src/components/email-editor/blocks/index.ts +9 -0
- package/src/components/email-editor/blocks/metrics-block.tsx +198 -0
- package/src/components/email-editor/blocks/social-block.tsx +75 -0
- package/src/components/email-editor/blocks/spacer-block.tsx +26 -0
- package/src/components/email-editor/blocks/text-block.tsx +75 -0
- package/src/components/email-editor/editor-sidebar.tsx +791 -0
- package/src/components/email-editor/email-editor.tsx +886 -0
- package/src/components/email-editor/index.ts +50 -0
- package/src/components/email-editor/renderer/block-renderers.ts +209 -0
- package/src/components/email-editor/renderer/email-html-renderer.ts +128 -0
- package/src/components/email-editor/types.ts +413 -0
- package/src/components/email-editor/utils/defaults.ts +116 -0
- package/src/components/email-editor/utils/undo-redo.ts +59 -0
- package/src/components/enrichment/EnrichButton.tsx +33 -0
- package/src/components/enrichment/EnrichmentProgress.tsx +66 -0
- package/src/components/enrichment/QualityBadge.tsx +43 -0
- package/src/components/enrichment/index.ts +8 -0
- package/src/components/gantt/GanttChart.tsx +25 -25
- package/src/components/gantt/types.ts +5 -5
- package/src/components/index.ts +46 -0
- package/src/components/integrations/ConnectionStatus.tsx +77 -0
- package/src/components/integrations/IntegrationCard.tsx +92 -0
- package/src/components/integrations/index.ts +5 -0
- package/src/components/kanban/KanbanBoard.tsx +103 -0
- package/src/components/kanban/index.ts +2 -0
- package/src/components/lists/CreateListDialog.tsx +158 -0
- package/src/components/lists/ListCard.tsx +77 -0
- package/src/components/lists/index.ts +5 -0
- package/src/components/pipeline/StageTransitionModal.tsx +146 -0
- package/src/components/pipeline/index.ts +2 -0
- package/src/components/settings/SettingsCard.tsx +33 -0
- package/src/components/settings/SettingsLayout.tsx +28 -0
- package/src/components/settings/SettingsNav.tsx +42 -0
- package/src/components/settings/index.ts +6 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { DividerBlock as DividerBlockType } from '../types'
|
|
4
|
+
|
|
5
|
+
interface DividerBlockProps {
|
|
6
|
+
block: DividerBlockType
|
|
7
|
+
onChange: (block: DividerBlockType) => void
|
|
8
|
+
isEditing?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function DividerBlockEditor({ block, onChange, isEditing = true }: DividerBlockProps) {
|
|
12
|
+
const color = block.color || '#d1d5db'
|
|
13
|
+
const thickness = block.thickness || 1
|
|
14
|
+
const width = block.width || 100
|
|
15
|
+
|
|
16
|
+
if (!isEditing) {
|
|
17
|
+
if (block.dividerStyle === 'space') {
|
|
18
|
+
return <div style={{ height: 32 }} />
|
|
19
|
+
}
|
|
20
|
+
return (
|
|
21
|
+
<div className="my-4" style={{ textAlign: 'center' }}>
|
|
22
|
+
<hr
|
|
23
|
+
style={{
|
|
24
|
+
display: 'inline-block',
|
|
25
|
+
width: `${width}%`,
|
|
26
|
+
border: 'none',
|
|
27
|
+
borderTop: `${thickness}px ${block.dividerStyle} ${color}`,
|
|
28
|
+
margin: 0,
|
|
29
|
+
}}
|
|
30
|
+
/>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="p-4 space-y-3">
|
|
37
|
+
{/* Preview */}
|
|
38
|
+
<div className="py-2">
|
|
39
|
+
<DividerBlockEditor block={block} onChange={onChange} isEditing={false} />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { FooterBlock as FooterBlockType } from '../types'
|
|
4
|
+
|
|
5
|
+
interface FooterBlockProps {
|
|
6
|
+
block: FooterBlockType
|
|
7
|
+
onChange: (block: FooterBlockType) => void
|
|
8
|
+
isEditing?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function FooterBlockEditor({ block, onChange, isEditing = true }: FooterBlockProps) {
|
|
12
|
+
const alignClass =
|
|
13
|
+
block.alignment === 'left'
|
|
14
|
+
? 'text-left'
|
|
15
|
+
: block.alignment === 'right'
|
|
16
|
+
? 'text-right'
|
|
17
|
+
: 'text-center'
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={`py-4 ${alignClass}`}>
|
|
21
|
+
<div className="text-xs text-gray-500 space-y-1">
|
|
22
|
+
<p>{block.companyName}</p>
|
|
23
|
+
{block.address && <p>{block.address}</p>}
|
|
24
|
+
{block.showUnsubscribe && (
|
|
25
|
+
<p className="mt-2">
|
|
26
|
+
<a
|
|
27
|
+
href={block.unsubscribeUrl || '#'}
|
|
28
|
+
className="text-gray-500 underline"
|
|
29
|
+
onClick={isEditing ? (e) => e.preventDefault() : undefined}
|
|
30
|
+
>
|
|
31
|
+
Unsubscribe
|
|
32
|
+
</a>
|
|
33
|
+
{' '}from these emails
|
|
34
|
+
</p>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { HeaderBlock as HeaderBlockType } from '../types'
|
|
4
|
+
import { Building2 } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
interface HeaderBlockProps {
|
|
7
|
+
block: HeaderBlockType
|
|
8
|
+
onChange: (block: HeaderBlockType) => void
|
|
9
|
+
isEditing?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function HeaderBlockEditor({ block, onChange, isEditing = true }: HeaderBlockProps) {
|
|
13
|
+
const alignClass =
|
|
14
|
+
block.alignment === 'left'
|
|
15
|
+
? 'text-left'
|
|
16
|
+
: block.alignment === 'right'
|
|
17
|
+
? 'text-right'
|
|
18
|
+
: 'text-center'
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className={`py-4 ${alignClass}`}>
|
|
22
|
+
<div className="inline-flex items-center gap-3">
|
|
23
|
+
{block.logoUrl ? (
|
|
24
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
25
|
+
<img
|
|
26
|
+
src={block.logoUrl}
|
|
27
|
+
alt={block.companyName}
|
|
28
|
+
className="h-10 w-auto"
|
|
29
|
+
/>
|
|
30
|
+
) : (
|
|
31
|
+
<div className="h-10 w-10 rounded bg-gray-100 flex items-center justify-center">
|
|
32
|
+
<Building2 className="h-5 w-5 text-gray-400" />
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
<span className="text-xl font-semibold">{block.companyName}</span>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { ImageBlock as ImageBlockType } from '../types'
|
|
4
|
+
import { ImageIcon } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
interface ImageBlockProps {
|
|
7
|
+
block: ImageBlockType
|
|
8
|
+
onChange: (block: ImageBlockType) => void
|
|
9
|
+
isEditing?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ImageBlockEditor({ block, onChange, isEditing = true }: ImageBlockProps) {
|
|
13
|
+
const alignClass =
|
|
14
|
+
block.alignment === 'left'
|
|
15
|
+
? 'text-left'
|
|
16
|
+
: block.alignment === 'right'
|
|
17
|
+
? 'text-right'
|
|
18
|
+
: 'text-center'
|
|
19
|
+
|
|
20
|
+
const widthPct = block.width || 100
|
|
21
|
+
|
|
22
|
+
if (!block.url) {
|
|
23
|
+
return (
|
|
24
|
+
<div className={`py-4 ${alignClass}`}>
|
|
25
|
+
<div
|
|
26
|
+
className="inline-flex flex-col items-center justify-center bg-gray-100 rounded-lg p-8"
|
|
27
|
+
style={{ width: `${widthPct}%`, maxWidth: '100%' }}
|
|
28
|
+
>
|
|
29
|
+
<ImageIcon className="h-12 w-12 text-gray-400" />
|
|
30
|
+
<p className="text-sm text-gray-500 mt-2">
|
|
31
|
+
{isEditing ? 'Click to set image URL' : 'No image set'}
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const imgElement = (
|
|
39
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
40
|
+
<img
|
|
41
|
+
src={block.url}
|
|
42
|
+
alt={block.alt || 'Email image'}
|
|
43
|
+
style={{ width: `${widthPct}%`, maxWidth: '100%', borderRadius: 4 }}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={`py-2 ${alignClass}`}>
|
|
49
|
+
{block.linkUrl && !isEditing ? (
|
|
50
|
+
<a href={block.linkUrl} target="_blank" rel="noopener noreferrer">
|
|
51
|
+
{imgElement}
|
|
52
|
+
</a>
|
|
53
|
+
) : (
|
|
54
|
+
imgElement
|
|
55
|
+
)}
|
|
56
|
+
{block.caption && (
|
|
57
|
+
<p className="text-sm text-gray-500 mt-1">{block.caption}</p>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
)
|
|
61
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { TextBlockEditor } from './text-block'
|
|
2
|
+
export { MetricsBlockEditor } from './metrics-block'
|
|
3
|
+
export { DividerBlockEditor } from './divider-block'
|
|
4
|
+
export { ButtonBlockEditor } from './button-block'
|
|
5
|
+
export { ImageBlockEditor } from './image-block'
|
|
6
|
+
export { SpacerBlockEditor } from './spacer-block'
|
|
7
|
+
export { SocialBlockEditor } from './social-block'
|
|
8
|
+
export { HeaderBlockEditor } from './header-block'
|
|
9
|
+
export { FooterBlockEditor } from './footer-block'
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { MetricsBlock as MetricsBlockType, MetricItem } from '../types'
|
|
4
|
+
import { Button } from '../../ui/button'
|
|
5
|
+
import { Input } from '../../ui/input'
|
|
6
|
+
import { Label } from '../../ui/label'
|
|
7
|
+
import {
|
|
8
|
+
Select,
|
|
9
|
+
SelectContent,
|
|
10
|
+
SelectItem,
|
|
11
|
+
SelectTrigger,
|
|
12
|
+
SelectValue,
|
|
13
|
+
} from '../../ui/select'
|
|
14
|
+
import { Plus, Trash2, TrendingUp, TrendingDown, Minus } from 'lucide-react'
|
|
15
|
+
import { cn } from '../../../lib/utils'
|
|
16
|
+
|
|
17
|
+
interface MetricsBlockProps {
|
|
18
|
+
block: MetricsBlockType
|
|
19
|
+
onChange: (block: MetricsBlockType) => void
|
|
20
|
+
isEditing?: boolean
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function MetricsBlockEditor({ block, onChange, isEditing = true }: MetricsBlockProps) {
|
|
24
|
+
const updateMetric = (metricId: string, updates: Partial<MetricItem>) => {
|
|
25
|
+
onChange({
|
|
26
|
+
...block,
|
|
27
|
+
metrics: block.metrics.map((m) =>
|
|
28
|
+
m.id === metricId ? { ...m, ...updates } : m
|
|
29
|
+
),
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const addMetric = () => {
|
|
34
|
+
const newMetric: MetricItem = {
|
|
35
|
+
id: `metric-${Date.now()}`,
|
|
36
|
+
label: 'New Metric',
|
|
37
|
+
value: '0',
|
|
38
|
+
change: '+0%',
|
|
39
|
+
changeType: 'neutral',
|
|
40
|
+
}
|
|
41
|
+
onChange({ ...block, metrics: [...block.metrics, newMetric] })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const removeMetric = (metricId: string) => {
|
|
45
|
+
onChange({ ...block, metrics: block.metrics.filter((m) => m.id !== metricId) })
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const getChangeColor = (type?: 'positive' | 'negative' | 'neutral') => {
|
|
49
|
+
switch (type) {
|
|
50
|
+
case 'positive':
|
|
51
|
+
return 'text-green-600'
|
|
52
|
+
case 'negative':
|
|
53
|
+
return 'text-red-600'
|
|
54
|
+
default:
|
|
55
|
+
return 'text-gray-500'
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const getChangeIcon = (type?: 'positive' | 'negative' | 'neutral') => {
|
|
60
|
+
switch (type) {
|
|
61
|
+
case 'positive':
|
|
62
|
+
return <TrendingUp className="h-3 w-3 text-green-600" />
|
|
63
|
+
case 'negative':
|
|
64
|
+
return <TrendingDown className="h-3 w-3 text-red-600" />
|
|
65
|
+
default:
|
|
66
|
+
return <Minus className="h-3 w-3 text-gray-400" />
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!isEditing) {
|
|
71
|
+
return (
|
|
72
|
+
<div className="py-4">
|
|
73
|
+
{block.title && (
|
|
74
|
+
<h3 className="text-lg font-semibold mb-4 text-center">{block.title}</h3>
|
|
75
|
+
)}
|
|
76
|
+
<div
|
|
77
|
+
className={cn(
|
|
78
|
+
'grid gap-4',
|
|
79
|
+
block.columns === 2 && 'grid-cols-2',
|
|
80
|
+
block.columns === 3 && 'grid-cols-3',
|
|
81
|
+
block.columns === 4 && 'grid-cols-4'
|
|
82
|
+
)}
|
|
83
|
+
>
|
|
84
|
+
{block.metrics.map((metric) => (
|
|
85
|
+
<div key={metric.id} className="text-center p-4 bg-gray-50 rounded-lg">
|
|
86
|
+
<div className="text-2xl font-bold">{metric.value}</div>
|
|
87
|
+
<div className="text-sm text-muted-foreground">{metric.label}</div>
|
|
88
|
+
{metric.change && (
|
|
89
|
+
<div
|
|
90
|
+
className={cn(
|
|
91
|
+
'flex items-center justify-center gap-1 text-sm mt-1',
|
|
92
|
+
getChangeColor(metric.changeType)
|
|
93
|
+
)}
|
|
94
|
+
>
|
|
95
|
+
{getChangeIcon(metric.changeType)}
|
|
96
|
+
{metric.change}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
))}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="space-y-4 p-4">
|
|
108
|
+
<div className="grid grid-cols-2 gap-4">
|
|
109
|
+
<div className="space-y-2">
|
|
110
|
+
<Label className="text-xs">Section Title</Label>
|
|
111
|
+
<Input
|
|
112
|
+
value={block.title || ''}
|
|
113
|
+
onChange={(e) => onChange({ ...block, title: e.target.value })}
|
|
114
|
+
placeholder="Key Metrics"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
<div className="space-y-2">
|
|
118
|
+
<Label className="text-xs">Columns</Label>
|
|
119
|
+
<Select
|
|
120
|
+
value={block.columns.toString()}
|
|
121
|
+
onValueChange={(v) =>
|
|
122
|
+
onChange({ ...block, columns: parseInt(v) as 2 | 3 | 4 })
|
|
123
|
+
}
|
|
124
|
+
>
|
|
125
|
+
<SelectTrigger>
|
|
126
|
+
<SelectValue />
|
|
127
|
+
</SelectTrigger>
|
|
128
|
+
<SelectContent>
|
|
129
|
+
<SelectItem value="2">2 columns</SelectItem>
|
|
130
|
+
<SelectItem value="3">3 columns</SelectItem>
|
|
131
|
+
<SelectItem value="4">4 columns</SelectItem>
|
|
132
|
+
</SelectContent>
|
|
133
|
+
</Select>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div className="space-y-2">
|
|
138
|
+
{block.metrics.map((metric) => (
|
|
139
|
+
<div
|
|
140
|
+
key={metric.id}
|
|
141
|
+
className="flex items-start gap-2 p-2 border rounded bg-background"
|
|
142
|
+
>
|
|
143
|
+
<div className="flex-1 grid grid-cols-4 gap-2">
|
|
144
|
+
<Input
|
|
145
|
+
value={metric.label}
|
|
146
|
+
onChange={(e) => updateMetric(metric.id, { label: e.target.value })}
|
|
147
|
+
placeholder="Label"
|
|
148
|
+
className="text-xs"
|
|
149
|
+
/>
|
|
150
|
+
<Input
|
|
151
|
+
value={metric.value}
|
|
152
|
+
onChange={(e) => updateMetric(metric.id, { value: e.target.value })}
|
|
153
|
+
placeholder="Value"
|
|
154
|
+
className="text-xs"
|
|
155
|
+
/>
|
|
156
|
+
<Input
|
|
157
|
+
value={metric.change || ''}
|
|
158
|
+
onChange={(e) => updateMetric(metric.id, { change: e.target.value })}
|
|
159
|
+
placeholder="+15%"
|
|
160
|
+
className="text-xs"
|
|
161
|
+
/>
|
|
162
|
+
<Select
|
|
163
|
+
value={metric.changeType || 'neutral'}
|
|
164
|
+
onValueChange={(v) =>
|
|
165
|
+
updateMetric(metric.id, {
|
|
166
|
+
changeType: v as 'positive' | 'negative' | 'neutral',
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
>
|
|
170
|
+
<SelectTrigger className="text-xs">
|
|
171
|
+
<SelectValue />
|
|
172
|
+
</SelectTrigger>
|
|
173
|
+
<SelectContent>
|
|
174
|
+
<SelectItem value="positive">Positive</SelectItem>
|
|
175
|
+
<SelectItem value="negative">Negative</SelectItem>
|
|
176
|
+
<SelectItem value="neutral">Neutral</SelectItem>
|
|
177
|
+
</SelectContent>
|
|
178
|
+
</Select>
|
|
179
|
+
</div>
|
|
180
|
+
<Button
|
|
181
|
+
variant="ghost"
|
|
182
|
+
size="icon"
|
|
183
|
+
className="h-8 w-8"
|
|
184
|
+
onClick={() => removeMetric(metric.id)}
|
|
185
|
+
disabled={block.metrics.length <= 1}
|
|
186
|
+
>
|
|
187
|
+
<Trash2 className="h-3 w-3 text-muted-foreground" />
|
|
188
|
+
</Button>
|
|
189
|
+
</div>
|
|
190
|
+
))}
|
|
191
|
+
<Button variant="outline" size="sm" onClick={addMetric} className="w-full">
|
|
192
|
+
<Plus className="mr-2 h-3 w-3" />
|
|
193
|
+
Add Metric
|
|
194
|
+
</Button>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
)
|
|
198
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { SocialBlock as SocialBlockType, SocialLink } from '../types'
|
|
5
|
+
import { Linkedin, Twitter, Facebook, Instagram, Youtube, Github, Globe } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
interface SocialBlockProps {
|
|
8
|
+
block: SocialBlockType
|
|
9
|
+
onChange: (block: SocialBlockType) => void
|
|
10
|
+
isEditing?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const PLATFORM_ICONS: Record<SocialLink['platform'], React.ComponentType<{ className?: string; style?: React.CSSProperties }>> = {
|
|
14
|
+
linkedin: Linkedin,
|
|
15
|
+
twitter: Twitter,
|
|
16
|
+
facebook: Facebook,
|
|
17
|
+
instagram: Instagram,
|
|
18
|
+
youtube: Youtube,
|
|
19
|
+
github: Github,
|
|
20
|
+
website: Globe,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const PLATFORM_COLORS: Record<SocialLink['platform'], string> = {
|
|
24
|
+
linkedin: '#0A66C2',
|
|
25
|
+
twitter: '#1DA1F2',
|
|
26
|
+
facebook: '#1877F2',
|
|
27
|
+
instagram: '#E4405F',
|
|
28
|
+
youtube: '#FF0000',
|
|
29
|
+
github: '#333333',
|
|
30
|
+
website: '#6b7280',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function SocialBlockEditor({ block, onChange, isEditing = true }: SocialBlockProps) {
|
|
34
|
+
const iconSize = block.iconSize || 24
|
|
35
|
+
const alignClass =
|
|
36
|
+
block.alignment === 'left'
|
|
37
|
+
? 'justify-start'
|
|
38
|
+
: block.alignment === 'right'
|
|
39
|
+
? 'justify-end'
|
|
40
|
+
: 'justify-center'
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className={`flex gap-3 py-3 ${alignClass}`}>
|
|
44
|
+
{block.links.map((link) => {
|
|
45
|
+
const Icon = PLATFORM_ICONS[link.platform]
|
|
46
|
+
const color = PLATFORM_COLORS[link.platform]
|
|
47
|
+
if (isEditing || !link.url) {
|
|
48
|
+
return (
|
|
49
|
+
<span
|
|
50
|
+
key={link.id}
|
|
51
|
+
className="inline-flex items-center justify-center rounded"
|
|
52
|
+
style={{ width: iconSize + 8, height: iconSize + 8 }}
|
|
53
|
+
>
|
|
54
|
+
<Icon style={{ width: iconSize, height: iconSize, color }} />
|
|
55
|
+
</span>
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
return (
|
|
59
|
+
<a
|
|
60
|
+
key={link.id}
|
|
61
|
+
href={link.url}
|
|
62
|
+
target="_blank"
|
|
63
|
+
rel="noopener noreferrer"
|
|
64
|
+
className="inline-flex items-center justify-center rounded"
|
|
65
|
+
style={{ width: iconSize + 8, height: iconSize + 8 }}
|
|
66
|
+
>
|
|
67
|
+
<Icon style={{ width: iconSize, height: iconSize, color }} />
|
|
68
|
+
</a>
|
|
69
|
+
)
|
|
70
|
+
})}
|
|
71
|
+
</div>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export { PLATFORM_ICONS, PLATFORM_COLORS }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { SpacerBlock as SpacerBlockType } from '../types'
|
|
4
|
+
|
|
5
|
+
interface SpacerBlockProps {
|
|
6
|
+
block: SpacerBlockType
|
|
7
|
+
onChange: (block: SpacerBlockType) => void
|
|
8
|
+
isEditing?: boolean
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SpacerBlockEditor({ block, onChange, isEditing = true }: SpacerBlockProps) {
|
|
12
|
+
const height = block.height || 32
|
|
13
|
+
|
|
14
|
+
if (!isEditing) {
|
|
15
|
+
return <div style={{ height }} />
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className="relative border border-dashed border-gray-300 rounded flex items-center justify-center"
|
|
21
|
+
style={{ height }}
|
|
22
|
+
>
|
|
23
|
+
<span className="text-xs text-gray-400">{height}px</span>
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { TextBlock as TextBlockType } from '../types'
|
|
5
|
+
|
|
6
|
+
interface TextBlockProps {
|
|
7
|
+
block: TextBlockType
|
|
8
|
+
onChange: (block: TextBlockType) => void
|
|
9
|
+
isEditing?: boolean
|
|
10
|
+
/** Optional custom rich text editor component. Receives content, onChange, placeholder, and className. */
|
|
11
|
+
renderEditor?: (props: {
|
|
12
|
+
content: string
|
|
13
|
+
onChange: (html: string) => void
|
|
14
|
+
placeholder?: string
|
|
15
|
+
className?: string
|
|
16
|
+
}) => React.ReactNode
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function TextBlockEditor({ block, onChange, isEditing = true, renderEditor }: TextBlockProps) {
|
|
20
|
+
if (!isEditing) {
|
|
21
|
+
return (
|
|
22
|
+
<div
|
|
23
|
+
className="prose prose-sm max-w-none py-2"
|
|
24
|
+
style={{
|
|
25
|
+
fontSize: block.fontSize ? `${block.fontSize}px` : undefined,
|
|
26
|
+
fontFamily: block.fontFamily || undefined,
|
|
27
|
+
lineHeight: block.lineHeight || undefined,
|
|
28
|
+
color: block.textColor || undefined,
|
|
29
|
+
}}
|
|
30
|
+
dangerouslySetInnerHTML={{ __html: block.content || '<p></p>' }}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// If a custom editor is provided, use it
|
|
36
|
+
if (renderEditor) {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
style={{
|
|
40
|
+
fontSize: block.fontSize ? `${block.fontSize}px` : undefined,
|
|
41
|
+
fontFamily: block.fontFamily || undefined,
|
|
42
|
+
lineHeight: block.lineHeight || undefined,
|
|
43
|
+
color: block.textColor || undefined,
|
|
44
|
+
}}
|
|
45
|
+
>
|
|
46
|
+
{renderEditor({
|
|
47
|
+
content: block.content,
|
|
48
|
+
onChange: (html) => onChange({ ...block, content: html }),
|
|
49
|
+
placeholder: 'Write your content...',
|
|
50
|
+
className: 'min-h-[60px] border-0',
|
|
51
|
+
})}
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Default fallback: simple contentEditable div
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
style={{
|
|
60
|
+
fontSize: block.fontSize ? `${block.fontSize}px` : undefined,
|
|
61
|
+
fontFamily: block.fontFamily || undefined,
|
|
62
|
+
lineHeight: block.lineHeight || undefined,
|
|
63
|
+
color: block.textColor || undefined,
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<div
|
|
67
|
+
contentEditable
|
|
68
|
+
suppressContentEditableWarning
|
|
69
|
+
className="min-h-[60px] outline-none prose prose-sm max-w-none py-2"
|
|
70
|
+
dangerouslySetInnerHTML={{ __html: block.content || '' }}
|
|
71
|
+
onBlur={(e) => onChange({ ...block, content: e.currentTarget.innerHTML })}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
)
|
|
75
|
+
}
|