@startsimpli/ui 0.4.6 → 0.4.8
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/__mocks__/next/link.js +11 -0
- package/src/components/ActivityTimeline.tsx +173 -0
- package/src/components/LogActivityDialog.tsx +303 -0
- package/src/components/QuickLogButtons.tsx +32 -0
- package/src/components/account/__tests__/account.test.tsx +315 -0
- package/src/components/badge/StageBadge.tsx +31 -0
- package/src/components/badge/index.ts +3 -0
- package/src/components/command-palette/CommandGroup.tsx +23 -0
- package/src/components/command-palette/CommandPalette.tsx +327 -0
- package/src/components/command-palette/CommandResultItem.tsx +59 -0
- package/src/components/command-palette/__tests__/CommandGroup.test.tsx +81 -0
- package/src/components/command-palette/__tests__/CommandResultItem.test.tsx +166 -0
- package/src/components/command-palette/__tests__/command-palette-context.test.tsx +166 -0
- package/src/components/command-palette/__tests__/useCommandPaletteSearch.test.ts +271 -0
- package/src/components/command-palette/command-palette-context.tsx +51 -0
- package/src/components/command-palette/index.ts +9 -0
- package/src/components/command-palette/useCommandPaletteSearch.ts +114 -0
- package/src/components/compose/__tests__/compose.test.tsx +656 -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/PipelineFunnel.tsx +126 -0
- package/src/components/dashboard/SparklineTrend.tsx +102 -0
- package/src/components/dashboard/TopCampaigns.tsx +132 -0
- package/src/components/dashboard/__tests__/dashboard.test.tsx +785 -0
- package/src/components/dashboard/index.ts +20 -0
- package/src/components/dialog/ConfirmDialog.tsx +72 -0
- package/src/components/dialog/__tests__/ConfirmDialog.test.tsx +126 -0
- package/src/components/dialog/index.ts +3 -0
- package/src/components/email-dialogs/__tests__/email-dialogs.test.tsx +982 -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/BlockRenderer.tsx +120 -0
- package/src/components/email-editor/__tests__/BlockRenderer.test.tsx +332 -0
- package/src/components/email-editor/__tests__/block-renderers.test.ts +624 -0
- package/src/components/email-editor/__tests__/email-html-renderer.test.ts +376 -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/__tests__/blocks.test.tsx +818 -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 +66 -0
- package/src/components/email-editor/email-editor.tsx +497 -0
- package/src/components/email-editor/hooks/__tests__/useDragDrop.test.ts +355 -0
- package/src/components/email-editor/hooks/__tests__/useEmailEditorState.test.ts +551 -0
- package/src/components/email-editor/hooks/useDragDrop.ts +181 -0
- package/src/components/email-editor/hooks/useEmailEditorState.ts +426 -0
- package/src/components/email-editor/index.ts +51 -0
- package/src/components/email-editor/panels/BlockPropertyPanel.tsx +637 -0
- package/src/components/email-editor/panels/GlobalStylesPanel.tsx +108 -0
- package/src/components/email-editor/panels/SectionSettingsPanel.tsx +80 -0
- package/src/components/email-editor/panels/__tests__/BlockPropertyPanel.test.tsx +707 -0
- package/src/components/email-editor/panels/__tests__/GlobalStylesPanel.test.tsx +226 -0
- package/src/components/email-editor/panels/index.ts +3 -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/__tests__/enrichment.test.tsx +184 -0
- package/src/components/enrichment/index.ts +8 -0
- package/src/components/gantt/GanttBoardView.tsx +71 -0
- package/src/components/gantt/GanttChart.tsx +140 -887
- package/src/components/gantt/GanttFilterBar.tsx +100 -0
- package/src/components/gantt/GanttListView.tsx +63 -0
- package/src/components/gantt/GanttTimelineView.tsx +215 -0
- package/src/components/gantt/__tests__/GanttBoardView.test.tsx +305 -0
- package/src/components/gantt/__tests__/GanttFilterBar.test.tsx +544 -0
- package/src/components/gantt/__tests__/GanttListView.test.tsx +337 -0
- package/src/components/gantt/__tests__/GanttTimelineView.test.tsx +375 -0
- package/src/components/gantt/__tests__/gantt-utils.test.ts +341 -0
- package/src/components/gantt/__tests__/useGanttState.test.ts +535 -0
- package/src/components/gantt/hooks/useGanttState.ts +644 -0
- package/src/components/gantt/index.ts +10 -0
- 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/__tests__/integrations.test.tsx +191 -0
- package/src/components/integrations/index.ts +5 -0
- package/src/components/kanban/KanbanBoard.tsx +103 -0
- package/src/components/kanban/__tests__/kanban.test.tsx +157 -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/__tests__/lists.test.tsx +263 -0
- package/src/components/lists/index.ts +5 -0
- package/src/components/loading/__tests__/loading.test.tsx +114 -0
- package/src/components/navigation/__tests__/navigation.test.tsx +194 -0
- package/src/components/pipeline/StageTransitionModal.tsx +146 -0
- package/src/components/pipeline/__tests__/pipeline.test.tsx +169 -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/__tests__/settings.test.tsx +181 -0
- package/src/components/settings/index.ts +6 -0
- package/src/components/wizard/__tests__/wizard.test.tsx +97 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@startsimpli/ui",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.8",
|
|
4
4
|
"description": "Shared UI components package for StartSimpli applications",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"./utils": "./src/utils/index.ts",
|
|
14
14
|
"./theme": "./src/theme/index.ts",
|
|
15
15
|
"./theme/contract": "./theme/contract.css",
|
|
16
|
+
"./email-editor": "./src/components/email-editor/index.ts",
|
|
16
17
|
"./tailwind": "./tailwind.preset.js"
|
|
17
18
|
},
|
|
18
19
|
"files": [
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const React = require('react')
|
|
2
|
+
|
|
3
|
+
// Simple passthrough mock for next/link
|
|
4
|
+
const Link = React.forwardRef(function Link({ href, children, className, ...props }, ref) {
|
|
5
|
+
return React.createElement('a', { href, className, ref, ...props }, children)
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
Link.displayName = 'Link'
|
|
9
|
+
|
|
10
|
+
module.exports = Link
|
|
11
|
+
module.exports.default = Link
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ComponentType } from 'react';
|
|
4
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
5
|
+
import { Clock, User } from 'lucide-react';
|
|
6
|
+
import { EmptyState } from './states';
|
|
7
|
+
|
|
8
|
+
export interface ActivityTimelineItem {
|
|
9
|
+
id: string;
|
|
10
|
+
type: string;
|
|
11
|
+
title: string;
|
|
12
|
+
description?: string;
|
|
13
|
+
occurredAt: string;
|
|
14
|
+
createdBy: { name: string };
|
|
15
|
+
durationMinutes?: number;
|
|
16
|
+
outcome?: string;
|
|
17
|
+
isAutomated?: boolean;
|
|
18
|
+
participants?: Array<{ name?: string }>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ActivityTimelineProps {
|
|
22
|
+
activities: ActivityTimelineItem[];
|
|
23
|
+
loading: boolean;
|
|
24
|
+
emptyTitle?: string;
|
|
25
|
+
emptyDescription?: string;
|
|
26
|
+
onLoadMore?: () => void;
|
|
27
|
+
hasMore?: boolean;
|
|
28
|
+
activityIcons?: Record<string, ComponentType<any>>;
|
|
29
|
+
activityColors?: Record<string, string>;
|
|
30
|
+
formatTime?: (date: string) => string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function defaultFormatTime(date: string): string {
|
|
34
|
+
try {
|
|
35
|
+
return formatDistanceToNow(new Date(date), { addSuffix: true });
|
|
36
|
+
} catch {
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function ActivityTimeline({
|
|
42
|
+
activities,
|
|
43
|
+
loading,
|
|
44
|
+
emptyTitle = 'No activities yet',
|
|
45
|
+
emptyDescription = 'Log calls, meetings, and notes to track your interactions',
|
|
46
|
+
onLoadMore,
|
|
47
|
+
hasMore = false,
|
|
48
|
+
activityIcons = {},
|
|
49
|
+
activityColors = {},
|
|
50
|
+
formatTime = defaultFormatTime,
|
|
51
|
+
}: ActivityTimelineProps) {
|
|
52
|
+
if (loading) {
|
|
53
|
+
return (
|
|
54
|
+
<div className="space-y-4">
|
|
55
|
+
{[...Array(3)].map((_, i) => (
|
|
56
|
+
<div key={i} className="flex gap-4 animate-pulse">
|
|
57
|
+
<div className="w-10 h-10 bg-gray-200 rounded-full" />
|
|
58
|
+
<div className="flex-1">
|
|
59
|
+
<div className="h-4 bg-gray-200 rounded w-1/3 mb-2" />
|
|
60
|
+
<div className="h-3 bg-gray-200 rounded w-2/3" />
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
))}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="space-y-4">
|
|
70
|
+
{/* Timeline */}
|
|
71
|
+
<div className="relative">
|
|
72
|
+
{/* Timeline line */}
|
|
73
|
+
<div className="absolute left-5 top-0 bottom-0 w-0.5 bg-gray-200" />
|
|
74
|
+
|
|
75
|
+
{/* Activities */}
|
|
76
|
+
<div className="space-y-6">
|
|
77
|
+
{activities.length === 0 ? (
|
|
78
|
+
<EmptyState title={emptyTitle} description={emptyDescription} />
|
|
79
|
+
) : (
|
|
80
|
+
activities.map((activity) => {
|
|
81
|
+
const Icon = activityIcons[activity.type];
|
|
82
|
+
const colorClass =
|
|
83
|
+
activityColors[activity.type] || 'bg-gray-100 text-gray-600';
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div key={activity.id} className="relative flex gap-4">
|
|
87
|
+
{/* Icon */}
|
|
88
|
+
<div
|
|
89
|
+
className={`relative z-10 flex items-center justify-center w-10 h-10 rounded-full ${colorClass}`}
|
|
90
|
+
>
|
|
91
|
+
{Icon ? <Icon className="w-5 h-5" /> : null}
|
|
92
|
+
</div>
|
|
93
|
+
|
|
94
|
+
{/* Content */}
|
|
95
|
+
<div className="flex-1 bg-white rounded-lg border border-gray-200 p-4">
|
|
96
|
+
<div className="flex items-start justify-between mb-2">
|
|
97
|
+
<div className="flex-1">
|
|
98
|
+
<h4 className="font-medium text-gray-900">
|
|
99
|
+
{activity.title}
|
|
100
|
+
</h4>
|
|
101
|
+
{activity.description && (
|
|
102
|
+
<p className="text-sm text-gray-600 mt-1">
|
|
103
|
+
{activity.description}
|
|
104
|
+
</p>
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
<div className="text-xs text-gray-500 ml-4 whitespace-nowrap">
|
|
108
|
+
{formatTime(activity.occurredAt)}
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* Metadata */}
|
|
113
|
+
<div className="flex items-center gap-4 text-sm text-gray-600">
|
|
114
|
+
<div className="flex items-center gap-1">
|
|
115
|
+
<User className="w-3 h-3" />
|
|
116
|
+
{activity.createdBy.name}
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{activity.durationMinutes && (
|
|
120
|
+
<div className="flex items-center gap-1">
|
|
121
|
+
<Clock className="w-3 h-3" />
|
|
122
|
+
{activity.durationMinutes}m
|
|
123
|
+
</div>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{activity.outcome && (
|
|
127
|
+
<div className="px-2 py-0.5 text-xs bg-gray-100 text-gray-700 rounded">
|
|
128
|
+
{activity.outcome.replace(/_/g, ' ')}
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
|
|
132
|
+
{activity.isAutomated && (
|
|
133
|
+
<div className="text-xs text-gray-500 italic">
|
|
134
|
+
Auto-tracked
|
|
135
|
+
</div>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
{/* Participants */}
|
|
140
|
+
{activity.participants &&
|
|
141
|
+
activity.participants.length > 0 && (
|
|
142
|
+
<div className="mt-2 pt-2 border-t border-gray-100">
|
|
143
|
+
<div className="text-xs text-gray-500">
|
|
144
|
+
With:{' '}
|
|
145
|
+
{activity.participants
|
|
146
|
+
.map((p) => p.name)
|
|
147
|
+
.filter(Boolean)
|
|
148
|
+
.join(', ')}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
</div>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
})
|
|
156
|
+
)}
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
{/* Load More */}
|
|
161
|
+
{hasMore && onLoadMore && (
|
|
162
|
+
<div className="text-center pt-4">
|
|
163
|
+
<button
|
|
164
|
+
onClick={onLoadMore}
|
|
165
|
+
className="px-4 py-2 text-sm text-primary-600 hover:text-primary-700 hover:bg-primary-50 rounded-md transition-colors"
|
|
166
|
+
>
|
|
167
|
+
Load more activities
|
|
168
|
+
</button>
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, ComponentType } from 'react';
|
|
4
|
+
import { X } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
export interface ActivityTypeOption {
|
|
7
|
+
value: string;
|
|
8
|
+
label: string;
|
|
9
|
+
icon: ComponentType<any>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface OutcomeOption {
|
|
13
|
+
value: string;
|
|
14
|
+
label: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LogActivityFormData {
|
|
18
|
+
type: string;
|
|
19
|
+
title: string;
|
|
20
|
+
description: string;
|
|
21
|
+
outcome: string | undefined;
|
|
22
|
+
durationMinutes: number | undefined;
|
|
23
|
+
occurredAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface LogActivityDialogProps {
|
|
27
|
+
activityTypes: ActivityTypeOption[];
|
|
28
|
+
outcomeOptions: OutcomeOption[];
|
|
29
|
+
defaultType?: string;
|
|
30
|
+
onSubmit: (data: LogActivityFormData) => Promise<void>;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
isSubmitting?: boolean;
|
|
33
|
+
/** Activity types for which outcome is required */
|
|
34
|
+
outcomeRequiredTypes?: string[];
|
|
35
|
+
/** Activity types for which duration field is shown */
|
|
36
|
+
durationTypes?: string[];
|
|
37
|
+
/** Activity types with follow-up checkbox, mapped to outcome values that trigger it */
|
|
38
|
+
followUpOutcomes?: string[];
|
|
39
|
+
title?: string;
|
|
40
|
+
submitLabel?: string;
|
|
41
|
+
submittingLabel?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function LogActivityDialog({
|
|
45
|
+
activityTypes,
|
|
46
|
+
outcomeOptions,
|
|
47
|
+
defaultType,
|
|
48
|
+
onSubmit,
|
|
49
|
+
onClose,
|
|
50
|
+
isSubmitting = false,
|
|
51
|
+
outcomeRequiredTypes = ['call', 'meeting'],
|
|
52
|
+
durationTypes = ['call', 'meeting'],
|
|
53
|
+
followUpOutcomes = ['callback_later', 'scheduled_meeting'],
|
|
54
|
+
title = 'Log Activity',
|
|
55
|
+
submitLabel = 'Log Activity',
|
|
56
|
+
submittingLabel = 'Logging...',
|
|
57
|
+
}: LogActivityDialogProps) {
|
|
58
|
+
const initialType = defaultType || activityTypes[0]?.value || '';
|
|
59
|
+
const [activityType, setActivityType] = useState(initialType);
|
|
60
|
+
const [formData, setFormData] = useState({
|
|
61
|
+
title: '',
|
|
62
|
+
description: '',
|
|
63
|
+
outcome: undefined as string | undefined,
|
|
64
|
+
durationMinutes: undefined as number | undefined,
|
|
65
|
+
occurredAt: new Date().toISOString().slice(0, 16),
|
|
66
|
+
});
|
|
67
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
68
|
+
const [createFollowUp, setCreateFollowUp] = useState(false);
|
|
69
|
+
|
|
70
|
+
const showOutcome = outcomeRequiredTypes.includes(activityType);
|
|
71
|
+
const showDuration = durationTypes.includes(activityType);
|
|
72
|
+
const showFollowUp =
|
|
73
|
+
followUpOutcomes.length > 0 &&
|
|
74
|
+
formData.outcome !== undefined &&
|
|
75
|
+
followUpOutcomes.includes(formData.outcome);
|
|
76
|
+
|
|
77
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
|
|
80
|
+
const newErrors: Record<string, string> = {};
|
|
81
|
+
if (!formData.title.trim()) {
|
|
82
|
+
newErrors.title = 'Title is required';
|
|
83
|
+
}
|
|
84
|
+
if (showOutcome && !formData.outcome) {
|
|
85
|
+
newErrors.outcome = 'Outcome is required for this activity type';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (Object.keys(newErrors).length > 0) {
|
|
89
|
+
setErrors(newErrors);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
await onSubmit({
|
|
95
|
+
type: activityType,
|
|
96
|
+
title: formData.title,
|
|
97
|
+
description: formData.description,
|
|
98
|
+
outcome: formData.outcome,
|
|
99
|
+
durationMinutes: formData.durationMinutes,
|
|
100
|
+
occurredAt: formData.occurredAt
|
|
101
|
+
? new Date(formData.occurredAt).toISOString()
|
|
102
|
+
: new Date().toISOString(),
|
|
103
|
+
});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Failed to log activity:', error);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const handleChange = (
|
|
110
|
+
field: keyof typeof formData,
|
|
111
|
+
value: string | number | undefined
|
|
112
|
+
) => {
|
|
113
|
+
setFormData((prev) => ({ ...prev, [field]: value }));
|
|
114
|
+
if (errors[field]) {
|
|
115
|
+
setErrors((prev) => ({ ...prev, [field]: undefined as any }));
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const currentTypeLabel =
|
|
120
|
+
activityTypes.find((t) => t.value === activityType)?.label || '';
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
|
124
|
+
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
|
125
|
+
{/* Header */}
|
|
126
|
+
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
|
127
|
+
<h2 className="text-xl font-semibold text-gray-900">{title}</h2>
|
|
128
|
+
<button
|
|
129
|
+
onClick={onClose}
|
|
130
|
+
className="text-gray-400 hover:text-gray-600 transition-colors"
|
|
131
|
+
>
|
|
132
|
+
<X className="w-5 h-5" />
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* Form */}
|
|
137
|
+
<form onSubmit={handleSubmit} className="p-6 space-y-4">
|
|
138
|
+
{/* Activity Type Selection */}
|
|
139
|
+
<div>
|
|
140
|
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
141
|
+
Activity Type *
|
|
142
|
+
</label>
|
|
143
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
|
144
|
+
{activityTypes.map(({ value, label, icon: Icon }) => (
|
|
145
|
+
<button
|
|
146
|
+
key={value}
|
|
147
|
+
type="button"
|
|
148
|
+
onClick={() => setActivityType(value)}
|
|
149
|
+
className={`flex items-center justify-center gap-2 px-4 py-3 text-sm font-medium rounded-lg border-2 transition-colors ${
|
|
150
|
+
activityType === value
|
|
151
|
+
? 'border-primary-600 bg-primary-50 text-primary-700'
|
|
152
|
+
: 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50'
|
|
153
|
+
}`}
|
|
154
|
+
>
|
|
155
|
+
<Icon className="w-4 h-4" />
|
|
156
|
+
{label}
|
|
157
|
+
</button>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
|
|
162
|
+
{/* Title */}
|
|
163
|
+
<div>
|
|
164
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
165
|
+
Title *
|
|
166
|
+
</label>
|
|
167
|
+
<input
|
|
168
|
+
type="text"
|
|
169
|
+
value={formData.title}
|
|
170
|
+
onChange={(e) => handleChange('title', e.target.value)}
|
|
171
|
+
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
|
172
|
+
errors.title ? 'border-red-500' : 'border-gray-300'
|
|
173
|
+
}`}
|
|
174
|
+
placeholder={`${currentTypeLabel} with...`}
|
|
175
|
+
/>
|
|
176
|
+
{errors.title && (
|
|
177
|
+
<p className="mt-1 text-sm text-red-600">{errors.title}</p>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{/* Date/Time and Duration */}
|
|
182
|
+
<div className="grid grid-cols-2 gap-4">
|
|
183
|
+
<div>
|
|
184
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
185
|
+
Date & Time
|
|
186
|
+
</label>
|
|
187
|
+
<input
|
|
188
|
+
type="datetime-local"
|
|
189
|
+
value={formData.occurredAt?.slice(0, 16) || ''}
|
|
190
|
+
onChange={(e) => handleChange('occurredAt', e.target.value)}
|
|
191
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{showDuration && (
|
|
196
|
+
<div>
|
|
197
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
198
|
+
Duration (minutes)
|
|
199
|
+
</label>
|
|
200
|
+
<input
|
|
201
|
+
type="number"
|
|
202
|
+
value={formData.durationMinutes || ''}
|
|
203
|
+
onChange={(e) =>
|
|
204
|
+
handleChange(
|
|
205
|
+
'durationMinutes',
|
|
206
|
+
e.target.value ? parseInt(e.target.value) : undefined
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
210
|
+
placeholder="30"
|
|
211
|
+
min="1"
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
{/* Outcome */}
|
|
218
|
+
{showOutcome && (
|
|
219
|
+
<div>
|
|
220
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
221
|
+
Outcome *
|
|
222
|
+
</label>
|
|
223
|
+
<select
|
|
224
|
+
value={formData.outcome || ''}
|
|
225
|
+
onChange={(e) =>
|
|
226
|
+
handleChange('outcome', e.target.value || undefined)
|
|
227
|
+
}
|
|
228
|
+
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
|
|
229
|
+
errors.outcome ? 'border-red-500' : 'border-gray-300'
|
|
230
|
+
}`}
|
|
231
|
+
>
|
|
232
|
+
<option value="">Select outcome...</option>
|
|
233
|
+
{outcomeOptions.map((outcome) => (
|
|
234
|
+
<option key={outcome.value} value={outcome.value}>
|
|
235
|
+
{outcome.label}
|
|
236
|
+
</option>
|
|
237
|
+
))}
|
|
238
|
+
</select>
|
|
239
|
+
{errors.outcome && (
|
|
240
|
+
<p className="mt-1 text-sm text-red-600">{errors.outcome}</p>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{/* Description */}
|
|
246
|
+
<div>
|
|
247
|
+
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
248
|
+
Notes
|
|
249
|
+
</label>
|
|
250
|
+
<textarea
|
|
251
|
+
value={formData.description || ''}
|
|
252
|
+
onChange={(e) => handleChange('description', e.target.value)}
|
|
253
|
+
rows={4}
|
|
254
|
+
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
255
|
+
placeholder="Add details about this activity..."
|
|
256
|
+
/>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* Follow-up Task */}
|
|
260
|
+
{showFollowUp && (
|
|
261
|
+
<div className="bg-primary-50 border border-primary-200 rounded-lg p-4">
|
|
262
|
+
<label className="flex items-center gap-2 cursor-pointer">
|
|
263
|
+
<input
|
|
264
|
+
type="checkbox"
|
|
265
|
+
checked={createFollowUp}
|
|
266
|
+
onChange={(e) => setCreateFollowUp(e.target.checked)}
|
|
267
|
+
className="w-4 h-4 text-primary-600 border-gray-300 rounded focus:ring-primary-500"
|
|
268
|
+
/>
|
|
269
|
+
<span className="text-sm font-medium text-blue-900">
|
|
270
|
+
Create follow-up task
|
|
271
|
+
</span>
|
|
272
|
+
</label>
|
|
273
|
+
{createFollowUp && (
|
|
274
|
+
<p className="text-xs text-blue-700 mt-2">
|
|
275
|
+
A task reminder will be created for this follow-up
|
|
276
|
+
</p>
|
|
277
|
+
)}
|
|
278
|
+
</div>
|
|
279
|
+
)}
|
|
280
|
+
|
|
281
|
+
{/* Actions */}
|
|
282
|
+
<div className="flex justify-end gap-3 pt-4">
|
|
283
|
+
<button
|
|
284
|
+
type="button"
|
|
285
|
+
onClick={onClose}
|
|
286
|
+
disabled={isSubmitting}
|
|
287
|
+
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
288
|
+
>
|
|
289
|
+
Cancel
|
|
290
|
+
</button>
|
|
291
|
+
<button
|
|
292
|
+
type="submit"
|
|
293
|
+
disabled={isSubmitting}
|
|
294
|
+
className="px-4 py-2 text-sm font-medium text-white bg-primary-600 rounded-md hover:bg-primary-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
295
|
+
>
|
|
296
|
+
{isSubmitting ? submittingLabel : submitLabel}
|
|
297
|
+
</button>
|
|
298
|
+
</div>
|
|
299
|
+
</form>
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ComponentType } from 'react';
|
|
4
|
+
|
|
5
|
+
export interface QuickLogAction {
|
|
6
|
+
type: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon: ComponentType<any>;
|
|
9
|
+
color: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface QuickLogButtonsProps {
|
|
13
|
+
actions: QuickLogAction[];
|
|
14
|
+
onAction: (type: string) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function QuickLogButtons({ actions, onAction }: QuickLogButtonsProps) {
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex flex-wrap gap-2">
|
|
20
|
+
{actions.map(({ type, label, icon: Icon, color }) => (
|
|
21
|
+
<button
|
|
22
|
+
key={type}
|
|
23
|
+
onClick={() => onAction(type)}
|
|
24
|
+
className={`flex items-center gap-2 px-4 py-2 text-sm font-medium text-white rounded-md transition-colors ${color}`}
|
|
25
|
+
>
|
|
26
|
+
<Icon className="w-4 h-4" />
|
|
27
|
+
{label}
|
|
28
|
+
</button>
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|