@myrjfa/ui 1.0.8 → 1.0.10
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/dist/chat/ChatMain.d.ts +9 -0
- package/dist/chat/ChatMain.d.ts.map +1 -0
- package/dist/chat/ChatMain.js +272 -0
- package/dist/chat/ChatSidebar.d.ts +13 -0
- package/dist/chat/ChatSidebar.d.ts.map +1 -0
- package/dist/chat/ChatSidebar.js +23 -0
- package/dist/chat/ConversationList.d.ts +12 -0
- package/dist/chat/ConversationList.d.ts.map +1 -0
- package/dist/chat/ConversationList.js +70 -0
- package/dist/chat/ExpensePanel.d.ts +11 -0
- package/dist/chat/ExpensePanel.d.ts.map +1 -0
- package/dist/chat/ExpensePanel.js +235 -0
- package/dist/chat/MessageBubble.d.ts +18 -0
- package/dist/chat/MessageBubble.d.ts.map +1 -0
- package/dist/chat/MessageBubble.js +284 -0
- package/dist/chat/MessageInput.d.ts +21 -0
- package/dist/chat/MessageInput.d.ts.map +1 -0
- package/dist/chat/MessageInput.js +252 -0
- package/dist/chat/MilestonePanel.d.ts +11 -0
- package/dist/chat/MilestonePanel.d.ts.map +1 -0
- package/dist/chat/MilestonePanel.js +173 -0
- package/dist/chat/components/ParticipantSelector.d.ts +11 -0
- package/dist/chat/components/ParticipantSelector.d.ts.map +1 -0
- package/dist/chat/components/ParticipantSelector.js +17 -0
- package/dist/chat/components/SplitInputs.d.ts +11 -0
- package/dist/chat/components/SplitInputs.d.ts.map +1 -0
- package/dist/chat/components/SplitInputs.js +83 -0
- package/dist/chat/components/SplitTypeSelector.d.ts +8 -0
- package/dist/chat/components/SplitTypeSelector.d.ts.map +1 -0
- package/dist/chat/components/SplitTypeSelector.js +13 -0
- package/dist/chat/dialogs/AddMemberDialog.d.ts +10 -0
- package/dist/chat/dialogs/AddMemberDialog.d.ts.map +1 -0
- package/dist/chat/dialogs/AddMemberDialog.js +50 -0
- package/dist/chat/dialogs/CameraDialog.d.ts +8 -0
- package/dist/chat/dialogs/CameraDialog.d.ts.map +1 -0
- package/dist/chat/dialogs/CameraDialog.js +80 -0
- package/dist/chat/dialogs/CreatePollDialog.d.ts +8 -0
- package/dist/chat/dialogs/CreatePollDialog.d.ts.map +1 -0
- package/dist/chat/dialogs/CreatePollDialog.js +38 -0
- package/dist/chat/dialogs/ExpenseAnalyticsDialog.d.ts +19 -0
- package/dist/chat/dialogs/ExpenseAnalyticsDialog.d.ts.map +1 -0
- package/dist/chat/dialogs/ExpenseAnalyticsDialog.js +52 -0
- package/dist/chat/dialogs/GroupInfoDialog.d.ts +10 -0
- package/dist/chat/dialogs/GroupInfoDialog.d.ts.map +1 -0
- package/dist/chat/dialogs/GroupInfoDialog.js +71 -0
- package/dist/chat/dialogs/GroupInfoViewDialog.d.ts +9 -0
- package/dist/chat/dialogs/GroupInfoViewDialog.d.ts.map +1 -0
- package/dist/chat/dialogs/GroupInfoViewDialog.js +15 -0
- package/dist/chat/dialogs/NewChatDialog.d.ts +2 -0
- package/dist/chat/dialogs/NewChatDialog.d.ts.map +1 -0
- package/dist/chat/dialogs/NewChatDialog.js +63 -0
- package/dist/chat/dialogs/NewGroupDialog.d.ts +2 -0
- package/dist/chat/dialogs/NewGroupDialog.d.ts.map +1 -0
- package/dist/chat/dialogs/NewGroupDialog.js +69 -0
- package/dist/chat/dialogs/ParticipantsDialog.d.ts +11 -0
- package/dist/chat/dialogs/ParticipantsDialog.d.ts.map +1 -0
- package/dist/chat/dialogs/ParticipantsDialog.js +93 -0
- package/dist/chat/dialogs/UserProfileDialog.d.ts +10 -0
- package/dist/chat/dialogs/UserProfileDialog.d.ts.map +1 -0
- package/dist/chat/dialogs/UserProfileDialog.js +100 -0
- package/dist/chat/index.d.ts +8 -0
- package/dist/chat/index.d.ts.map +1 -0
- package/dist/chat/index.js +7 -0
- package/dist/dashboard/action-buttons.js +4 -5
- package/dist/dashboard/app-sidebar.js +46 -128
- package/dist/dashboard/card-header.js +1 -2
- package/dist/dashboard/dashboard-components.js +3 -5
- package/dist/dashboard/data-table.d.ts +18 -18
- package/dist/dashboard/data-table.js +54 -66
- package/dist/dashboard/nav-main.js +4 -5
- package/dist/dashboard/nav-secondary.js +2 -25
- package/dist/dashboard/stats-card.js +1 -2
- package/dist/dashboard/table.js +8 -54
- package/dist/header/mobile-nav-item.js +1 -2
- package/dist/header/mobile-navbar.js +8 -9
- package/dist/header/nav-item.d.ts +2 -1
- package/dist/header/nav-item.d.ts.map +1 -1
- package/dist/header/nav-item.js +2 -3
- package/dist/index.d.ts +57 -56
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +57 -57
- package/dist/main/footer.js +5 -9
- package/dist/main/navbar.d.ts.map +1 -1
- package/dist/main/navbar.js +53 -136
- package/dist/main/paginationBar.js +12 -14
- package/dist/main/portfolio/portfolioItem.js +1 -2
- package/dist/main/portfolio/portfolioPopup.js +16 -17
- package/dist/main/review-page.d.ts.map +1 -1
- package/dist/main/review-page.js +131 -198
- package/dist/misc/accordion.js +8 -34
- package/dist/misc/alert-dialog.js +22 -55
- package/dist/misc/alert.js +7 -32
- package/dist/misc/alertdialogclaude.js +50 -125
- package/dist/misc/avatar.js +6 -31
- package/dist/misc/badge.d.ts +8 -8
- package/dist/misc/badge.js +4 -27
- package/dist/misc/button.d.ts +9 -9
- package/dist/misc/button.js +4 -27
- package/dist/misc/card.js +12 -40
- package/dist/misc/carousel.js +37 -61
- package/dist/misc/checkbox.js +2 -25
- package/dist/misc/command.d.ts +3 -1
- package/dist/misc/command.d.ts.map +1 -1
- package/dist/misc/command.js +18 -49
- package/dist/misc/datepicker.js +66 -76
- package/dist/misc/dialog.js +20 -52
- package/dist/misc/dropdown-menu.js +30 -67
- package/dist/misc/faq.js +2 -3
- package/dist/misc/file-preview.js +43 -97
- package/dist/misc/filter.d.ts.map +1 -1
- package/dist/misc/filter.js +18 -21
- package/dist/misc/filterBar.js +136 -160
- package/dist/misc/form.d.ts +1 -1
- package/dist/misc/form.d.ts.map +1 -1
- package/dist/misc/form.js +39 -60
- package/dist/misc/image-cropper.d.ts +10 -0
- package/dist/misc/image-cropper.d.ts.map +1 -0
- package/dist/misc/image-cropper.js +44 -0
- package/dist/misc/input.js +2 -25
- package/dist/misc/label.js +2 -25
- package/dist/misc/line-carousel.js +3 -13
- package/dist/misc/multiImageUploader.d.ts +8 -8
- package/dist/misc/multiImageUploader.js +10 -11
- package/dist/misc/newsLetter.js +18 -78
- package/dist/misc/notification.d.ts +1 -2
- package/dist/misc/notification.d.ts.map +1 -1
- package/dist/misc/notification.js +89 -125
- package/dist/misc/pagination.js +16 -45
- package/dist/misc/password-eye.js +2 -3
- package/dist/misc/popover.d.ts.map +1 -1
- package/dist/misc/popover.js +8 -34
- package/dist/misc/progress.js +2 -25
- package/dist/misc/scroll-area.js +5 -29
- package/dist/misc/searchBar.js +27 -31
- package/dist/misc/searchBox.js +18 -22
- package/dist/misc/select.d.ts.map +1 -1
- package/dist/misc/select.js +22 -54
- package/dist/misc/separator.js +2 -25
- package/dist/misc/share.js +48 -104
- package/dist/misc/sheet.js +21 -53
- package/dist/misc/sidebar.d.ts +69 -69
- package/dist/misc/sidebar.js +112 -153
- package/dist/misc/skeleton.d.ts +2 -2
- package/dist/misc/skeleton.js +6 -29
- package/dist/misc/slider.js +3 -26
- package/dist/misc/starRating.js +5 -15
- package/dist/misc/stateCard.js +7 -8
- package/dist/misc/switch.d.ts +5 -0
- package/dist/misc/switch.d.ts.map +1 -0
- package/dist/misc/switch.js +8 -0
- package/dist/misc/tabs.js +8 -34
- package/dist/misc/textarea.js +2 -25
- package/dist/misc/tile.js +31 -92
- package/dist/misc/tooltip.js +8 -34
- package/dist/profile/socialLink.d.ts +1 -1
- package/dist/profile/socialLink.d.ts.map +1 -1
- package/dist/profile/socialLink.js +53 -69
- package/dist/utils/permissions.d.ts +7 -0
- package/dist/utils/permissions.d.ts.map +1 -0
- package/dist/utils/permissions.js +38 -0
- package/package.json +8 -5
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useRef, useEffect } from "react";
|
|
4
|
+
import { Send, Smile, Image as ImageIcon, FileText, Camera, MapPin, Mic, X, CircleStopIcon, VideoIcon, PlusCircleIcon, ListTodo, ReceiptText, ChartBar } from "lucide-react";
|
|
5
|
+
import { Button } from "../misc/button";
|
|
6
|
+
import { Textarea } from "../misc/textarea";
|
|
7
|
+
import { Popover, PopoverContent, PopoverTrigger } from "../misc/popover";
|
|
8
|
+
import { Progress } from "../misc/progress";
|
|
9
|
+
import { CameraDialog } from "./dialogs/CameraDialog";
|
|
10
|
+
import { CreatePollDialog } from "./dialogs/CreatePollDialog";
|
|
11
|
+
import { showAlert } from "../misc/alertdialogclaude";
|
|
12
|
+
import { isCompressibleImage, compressImage } from "@myrjfa/state/dist/lib/utils/fileCompression";
|
|
13
|
+
import EmojiPicker from 'emoji-picker-react';
|
|
14
|
+
export function MessageInput({ onSend, onTypingStart, onTypingStop, disabled, replyTo, editMessage, onEdit, onCancelEdit, onCancelReply, onAddMilestone, onAddExpense, onCreatePoll, participants = [], // Added
|
|
15
|
+
activePanel = "chat" }) {
|
|
16
|
+
var _a, _b, _c;
|
|
17
|
+
const [text, setText] = useState(editMessage ? (_b = (_a = editMessage === null || editMessage === void 0 ? void 0 : editMessage.content) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : "" : "");
|
|
18
|
+
const [mentionSearch, setMentionSearch] = useState(null);
|
|
19
|
+
const [mentionIndex, setMentionIndex] = useState(-1);
|
|
20
|
+
const [selectedFiles, setSelectedFiles] = useState([]);
|
|
21
|
+
const [uploadProgress, setUploadProgress] = useState(null);
|
|
22
|
+
const [isSending, setIsSending] = useState(false);
|
|
23
|
+
const textareaRef = useRef(null);
|
|
24
|
+
const fileInputRef = useRef(null);
|
|
25
|
+
const [fileAcceptType, setFileAcceptType] = useState("");
|
|
26
|
+
const typingTimeoutRef = useRef(null);
|
|
27
|
+
const isTypingRef = useRef(false);
|
|
28
|
+
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
|
29
|
+
const [isCameraOpen, setIsCameraOpen] = useState(false);
|
|
30
|
+
const [isPollOpen, setIsPollOpen] = useState(false);
|
|
31
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
32
|
+
const mediaRecorderRef = useRef(null);
|
|
33
|
+
const audioChunksRef = useRef([]);
|
|
34
|
+
const [mentionCoords, setMentionCoords] = useState({ top: 0, left: 0 }); // Added
|
|
35
|
+
// Auto-resize textarea
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const el = textareaRef.current;
|
|
38
|
+
if (el) {
|
|
39
|
+
el.style.height = "auto";
|
|
40
|
+
el.style.height = Math.min(el.scrollHeight, 120) + "px";
|
|
41
|
+
}
|
|
42
|
+
}, [text]);
|
|
43
|
+
// Focus input when replyTo message is set
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
var _a;
|
|
46
|
+
if (replyTo) {
|
|
47
|
+
(_a = textareaRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
48
|
+
}
|
|
49
|
+
}, [replyTo]);
|
|
50
|
+
const handleTextChange = (e) => {
|
|
51
|
+
const value = e.target.value;
|
|
52
|
+
const cursorPosition = e.target.selectionStart;
|
|
53
|
+
setText(value);
|
|
54
|
+
// Mention detection
|
|
55
|
+
const textBeforeCursor = value.substring(0, cursorPosition);
|
|
56
|
+
const lastAtChar = textBeforeCursor.lastIndexOf("@");
|
|
57
|
+
if (lastAtChar !== -1 && (lastAtChar === 0 || textBeforeCursor[lastAtChar - 1] === " ")) {
|
|
58
|
+
const search = textBeforeCursor.substring(lastAtChar + 1);
|
|
59
|
+
if (!search.includes(" ")) {
|
|
60
|
+
setMentionSearch(search);
|
|
61
|
+
setMentionIndex(lastAtChar);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
setMentionSearch(null);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
setMentionSearch(null);
|
|
69
|
+
}
|
|
70
|
+
// Typing indicator
|
|
71
|
+
if (!isTypingRef.current && e.target.value.length > 0) {
|
|
72
|
+
isTypingRef.current = true;
|
|
73
|
+
onTypingStart === null || onTypingStart === void 0 ? void 0 : onTypingStart();
|
|
74
|
+
}
|
|
75
|
+
// Reset typing timeout
|
|
76
|
+
if (typingTimeoutRef.current)
|
|
77
|
+
clearTimeout(typingTimeoutRef.current);
|
|
78
|
+
typingTimeoutRef.current = setTimeout(() => {
|
|
79
|
+
isTypingRef.current = false;
|
|
80
|
+
onTypingStop === null || onTypingStop === void 0 ? void 0 : onTypingStop();
|
|
81
|
+
}, 2000);
|
|
82
|
+
};
|
|
83
|
+
const insertMention = (username) => {
|
|
84
|
+
var _a;
|
|
85
|
+
if (mentionIndex === -1)
|
|
86
|
+
return;
|
|
87
|
+
const before = text.substring(0, mentionIndex);
|
|
88
|
+
const after = text.substring(((_a = textareaRef.current) === null || _a === void 0 ? void 0 : _a.selectionStart) || text.length);
|
|
89
|
+
const newText = `${before}@${username} ${after}`;
|
|
90
|
+
setText(newText);
|
|
91
|
+
setMentionSearch(null);
|
|
92
|
+
// Set cursor after the inserted mention
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
if (textareaRef.current) {
|
|
95
|
+
const newCursorPos = before.length + `@${username} `.length;
|
|
96
|
+
textareaRef.current.selectionStart = newCursorPos;
|
|
97
|
+
textareaRef.current.selectionEnd = newCursorPos;
|
|
98
|
+
textareaRef.current.focus();
|
|
99
|
+
}
|
|
100
|
+
}, 0);
|
|
101
|
+
};
|
|
102
|
+
async function handleSend() {
|
|
103
|
+
var _a;
|
|
104
|
+
const trimmed = text.trim();
|
|
105
|
+
if ((!trimmed && selectedFiles.length === 0) || disabled)
|
|
106
|
+
return;
|
|
107
|
+
setIsSending(true);
|
|
108
|
+
setUploadProgress(0);
|
|
109
|
+
try {
|
|
110
|
+
let filesToSend = [...selectedFiles];
|
|
111
|
+
// Compress images
|
|
112
|
+
setUploadProgress(10); // Start progress for compression
|
|
113
|
+
const compressed = await Promise.all(filesToSend.map(async (file) => {
|
|
114
|
+
if (isCompressibleImage(file)) {
|
|
115
|
+
try {
|
|
116
|
+
return await compressImage(file);
|
|
117
|
+
}
|
|
118
|
+
catch (e) {
|
|
119
|
+
console.error("Compression failed for", file.name, e);
|
|
120
|
+
return file;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return file;
|
|
124
|
+
}));
|
|
125
|
+
setUploadProgress(30); // Compression done, starting upload
|
|
126
|
+
// Note: sendMessage doesn't currently support progress callback.
|
|
127
|
+
// We'll simulate progress or update it if chatApi supports it.
|
|
128
|
+
// For now, call sendMessage.
|
|
129
|
+
if (editMessage) {
|
|
130
|
+
await (onEdit === null || onEdit === void 0 ? void 0 : onEdit(editMessage._id, trimmed));
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
await onSend(trimmed, compressed);
|
|
134
|
+
}
|
|
135
|
+
setUploadProgress(100);
|
|
136
|
+
setText("");
|
|
137
|
+
setSelectedFiles([]);
|
|
138
|
+
// Clear typing
|
|
139
|
+
if (typingTimeoutRef.current)
|
|
140
|
+
clearTimeout(typingTimeoutRef.current);
|
|
141
|
+
isTypingRef.current = false;
|
|
142
|
+
onTypingStop === null || onTypingStop === void 0 ? void 0 : onTypingStop();
|
|
143
|
+
// Refocus
|
|
144
|
+
(_a = textareaRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
console.error(error);
|
|
148
|
+
showAlert("Failed to send message", "Error");
|
|
149
|
+
}
|
|
150
|
+
finally {
|
|
151
|
+
setIsSending(false);
|
|
152
|
+
setUploadProgress(null);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function handleKeyDown(e) {
|
|
156
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
157
|
+
e.preventDefault();
|
|
158
|
+
handleSend();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const handleFileChange = (e) => {
|
|
162
|
+
if (e.target.files) {
|
|
163
|
+
setSelectedFiles(prev => [...prev, ...Array.from(e.target.files)]);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const removeFile = (index) => {
|
|
167
|
+
setSelectedFiles(prev => prev.filter((_, i) => i !== index));
|
|
168
|
+
};
|
|
169
|
+
const handleLocation = () => {
|
|
170
|
+
if ("geolocation" in navigator) {
|
|
171
|
+
navigator.geolocation.getCurrentPosition((position) => {
|
|
172
|
+
const { latitude, longitude } = position.coords;
|
|
173
|
+
const mapLink = `https://www.google.com/maps?q=${latitude},${longitude}`;
|
|
174
|
+
onSend(mapLink);
|
|
175
|
+
}, (error) => {
|
|
176
|
+
console.error("Error getting location:", error);
|
|
177
|
+
showAlert("Could not get location.");
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
showAlert("Geolocation is not supported by this browser.");
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const handleCameraCapture = (file) => {
|
|
185
|
+
setSelectedFiles(prev => [...prev, file]);
|
|
186
|
+
};
|
|
187
|
+
const startRecording = async () => {
|
|
188
|
+
try {
|
|
189
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
190
|
+
const mediaRecorder = new MediaRecorder(stream);
|
|
191
|
+
mediaRecorderRef.current = mediaRecorder;
|
|
192
|
+
audioChunksRef.current = [];
|
|
193
|
+
mediaRecorder.ondataavailable = (event) => {
|
|
194
|
+
if (event.data.size > 0) {
|
|
195
|
+
audioChunksRef.current.push(event.data);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
mediaRecorder.onstop = () => {
|
|
199
|
+
const audioBlob = new Blob(audioChunksRef.current, { type: "audio/webm" });
|
|
200
|
+
const audioFile = new File([audioBlob], `voice_${Date.now()}.webm`, { type: "audio/webm" });
|
|
201
|
+
setSelectedFiles(prev => [...prev, audioFile]);
|
|
202
|
+
stream.getTracks().forEach(track => track.stop());
|
|
203
|
+
};
|
|
204
|
+
mediaRecorder.start();
|
|
205
|
+
setIsRecording(true);
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
console.error("Error accessing microphone:", err);
|
|
209
|
+
alert("Could not access microphone");
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
const stopRecording = () => {
|
|
213
|
+
if (mediaRecorderRef.current && isRecording) {
|
|
214
|
+
mediaRecorderRef.current.stop();
|
|
215
|
+
setIsRecording(false);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
const handleEmojiSelect = (emojiObject) => {
|
|
219
|
+
if (textareaRef.current) {
|
|
220
|
+
const cursorPos = textareaRef.current.selectionStart;
|
|
221
|
+
const newText = text.slice(0, cursorPos) + emojiObject.emoji + text.slice(cursorPos);
|
|
222
|
+
setText(newText);
|
|
223
|
+
setShowEmojiPicker(false);
|
|
224
|
+
textareaRef.current.focus();
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
// Only show message input for chat panel
|
|
228
|
+
const showMessageInput = activePanel === "chat";
|
|
229
|
+
return (_jsxs("div", { className: "border-t border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-900 p-3 relative", children: [_jsx(CameraDialog, { open: isCameraOpen, onOpenChange: setIsCameraOpen, onCapture: handleCameraCapture }), _jsx(CreatePollDialog, { open: isPollOpen, onOpenChange: setIsPollOpen, onCreate: (q, opts, multi) => {
|
|
230
|
+
onCreatePoll === null || onCreatePoll === void 0 ? void 0 : onCreatePoll(q, opts, multi);
|
|
231
|
+
} }), mentionSearch !== null && participants.length > 0 && (_jsxs("div", { className: "absolute bottom-full left-4 mb-2 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-xl w-48 py-1 z-50 overflow-y-auto max-h-48", children: [participants
|
|
232
|
+
.filter(p => !mentionSearch || p.userId.toLowerCase().includes(mentionSearch.toLowerCase()))
|
|
233
|
+
.map(p => (_jsx("button", { onClick: () => insertMention(p.userId), className: "w-full text-left px-3 py-2 text-sm hover:bg-indigo-50 dark:hover:bg-indigo-900/40 flex items-center gap-2", children: _jsxs("span", { className: "truncate", children: ["@", p.userId] }) }, p.userId))), participants.filter(p => !mentionSearch || p.userId.toLowerCase().includes(mentionSearch.toLowerCase())).length === 0 && (_jsx("div", { className: "px-3 py-2 text-xs text-gray-500 italic", children: "No members found" }))] })), selectedFiles.length > 0 && !editMessage && (_jsx("div", { className: "max-w-4xl mx-auto mb-2 flex flex-wrap gap-2", children: selectedFiles.map((file, i) => (_jsxs("div", { className: "flex items-center bg-gray-100 dark:bg-gray-800 rounded-lg px-2 py-1 text-xs text-gray-600 dark:text-gray-300", children: [_jsx("span", { className: "truncate max-w-[150px]", children: file.name }), _jsx("button", { onClick: () => removeFile(i), className: "ml-2 text-red-500 hover:text-red-700 font-bold", children: "\u00D7" })] }, i))) })), replyTo && (_jsxs("div", { className: "max-w-4xl mx-auto mb-2 flex items-center gap-2 bg-gray-100 dark:bg-gray-800 px-3 py-2 rounded-lg border-l-2 border-teal-500", children: [_jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("div", { className: "text-xs font-semibold text-teal-600 dark:text-teal-400", children: editMessage ? "Editing" : ("Replying to" + replyTo.senderId) }), _jsx("div", { className: "text-xs text-gray-600 dark:text-gray-400 truncate", children: (_c = replyTo.content.text) === null || _c === void 0 ? void 0 : _c.substring(0, 60) })] }), _jsx("button", { onClick: onCancelReply, className: "text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 flex-shrink-0", children: _jsx(X, { className: "w-4 h-4" }) })] })), _jsxs("div", { className: `${showMessageInput ? "flex" : "hidden md:flex"} items-end gap-2 max-w-4xl mx-auto`, children: [_jsx("input", { type: "file", ref: fileInputRef, onChange: handleFileChange, accept: fileAcceptType, multiple: true, className: "hidden" }), !editMessage && _jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full h-10 w-10 shrink-0", children: _jsx(PlusCircleIcon, { className: "w-5 h-5" }) }) }), _jsxs(PopoverContent, { side: "top", align: "start", className: "w-48 p-2 flex flex-col gap-1", children: [_jsxs(Button, { variant: "ghost", className: "justify-start gap-2 w-full", onClick: async () => {
|
|
234
|
+
var _a;
|
|
235
|
+
await setFileAcceptType(".pdf,.doc,.docx,.txt,.xlsx,.pptx");
|
|
236
|
+
(_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
237
|
+
}, children: [_jsx(FileText, { className: "w-4 h-4 text-purple-500" }), "Document"] }), _jsxs(Button, { variant: "ghost", className: "justify-start gap-2 w-full", onClick: async () => {
|
|
238
|
+
var _a;
|
|
239
|
+
await setFileAcceptType("image/*");
|
|
240
|
+
(_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
241
|
+
}, children: [_jsx(ImageIcon, { className: "w-4 h-4 text-blue-500" }), "Photos"] }), _jsxs(Button, { variant: "ghost", className: "justify-start gap-2 w-full", onClick: async () => {
|
|
242
|
+
var _a;
|
|
243
|
+
await setFileAcceptType("video/*");
|
|
244
|
+
(_a = fileInputRef.current) === null || _a === void 0 ? void 0 : _a.click();
|
|
245
|
+
}, children: [_jsx(VideoIcon, { className: "w-4 h-4 text-rose-500" }), "Videos"] }), _jsxs(Button, { variant: "ghost", className: "justify-start gap-2 w-full", onClick: handleLocation, children: [_jsx(MapPin, { className: "w-4 h-4 text-green-500" }), "Location"] }), _jsxs(Button, { variant: "ghost", className: "justify-start gap-2 w-full", onClick: () => setIsPollOpen(true), children: [_jsx(ChartBar, { className: "w-4 h-4 text-orange-500" }), "Poll"] }), _jsx("div", { className: "border-t border-gray-200 dark:border-gray-700 my-1" }), _jsxs(Button, { variant: "ghost", className: "justify-start gap-2 w-full text-teal-600 dark:text-teal-400", onClick: () => {
|
|
246
|
+
onAddMilestone === null || onAddMilestone === void 0 ? void 0 : onAddMilestone();
|
|
247
|
+
}, children: [_jsx(ListTodo, { className: "w-4 h-4" }), "Add Milestone"] }), _jsxs(Button, { variant: "ghost", className: "justify-start gap-2 w-full text-teal-600 dark:text-teal-400", onClick: () => {
|
|
248
|
+
onAddExpense === null || onAddExpense === void 0 ? void 0 : onAddExpense();
|
|
249
|
+
}, children: [_jsx(ReceiptText, { className: "w-4 h-4" }), "Add Expense"] })] })] }), _jsxs("div", { className: "flex-1 bg-gray-100 dark:bg-gray-800 rounded-2xl flex items-end relative", children: [_jsx(Textarea, { ref: textareaRef, value: text, onChange: handleTextChange, onKeyDown: handleKeyDown, placeholder: "Type a message...", disabled: disabled || isSending, rows: 1, className: "flex-1 bg-transparent border-none focus-visible:ring-0 px-4 py-3 text-[15px] resize-none text-gray-900 dark:text-white placeholder-gray-400 max-h-[120px] min-h-[48px]" }), uploadProgress !== null && (_jsx("div", { className: "absolute left-0 bottom-full w-full px-4 mb-2", children: _jsxs("div", { className: "bg-white dark:bg-gray-800 p-2 rounded-lg shadow-lg border border-gray-100 dark:border-gray-700", children: [_jsxs("div", { className: "flex justify-between items-center mb-1", children: [_jsx("span", { className: "text-[10px] text-gray-500 font-medium", children: "Uploading..." }), _jsxs("span", { className: "text-[10px] text-indigo-500 font-bold", children: [uploadProgress, "%"] })] }), _jsx(Progress, { value: uploadProgress, className: "h-1" })] }) })), _jsx(Button, { variant: "ghost", size: "icon", className: "mr-1 mb-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full hidden sm:inline-flex", onClick: () => setShowEmojiPicker(!showEmojiPicker), children: _jsx(Smile, { className: "w-5 h-5" }) }), !editMessage && _jsx(Button, { variant: "ghost", size: "icon", className: "mr-1 mb-1 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 rounded-full", onClick: () => setIsCameraOpen(true), children: _jsx(Camera, { className: "w-5 h-5" }) })] }), text.trim() || selectedFiles.length > 0 || editMessage ? (_jsx(Button, { onClick: handleSend, disabled: disabled, className: "rounded-full h-11 w-11 p-0 bg-gradient-to-r from-teal-500 to-cyan-600 hover:from-teal-600 hover:to-cyan-700 shadow-lg shadow-teal-500/25 shrink-0", children: _jsx(Send, { className: "w-5 h-5" }) })) : (_jsx(Button, { variant: "ghost", size: "icon", className: `rounded-full h-11 w-11 shrink-0 transition-all ${isRecording
|
|
250
|
+
? "bg-red-500 hover:bg-red-600 animate-pulse text-white"
|
|
251
|
+
: "text-teal-400 hover:bg-teal-100 dark:hover:bg-teal-800"}`, onClick: () => isRecording ? stopRecording() : startRecording(), children: isRecording ? _jsx(CircleStopIcon, { className: "w-6 h-6" }) : _jsx(Mic, { className: "w-6 h-6" }) })), editMessage && _jsx(Button, { variant: "ghost", size: "icon", className: "rounded-full h-11 w-11 shrink-0", onClick: onCancelEdit, children: _jsx(X, { className: "w-5 h-5" }) }), showEmojiPicker && _jsx(EmojiPicker, { onEmojiClick: handleEmojiSelect })] })] }));
|
|
252
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface MilestonePanelProps {
|
|
2
|
+
conversationId: string;
|
|
3
|
+
currentUserId: string;
|
|
4
|
+
isFullScreen?: boolean;
|
|
5
|
+
isFeatureEnabled?: boolean;
|
|
6
|
+
isAdmin?: boolean;
|
|
7
|
+
onEnableFeature?: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function MilestonePanel({ conversationId, currentUserId, isFullScreen, isFeatureEnabled, isAdmin, onEnableFeature }: MilestonePanelProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=MilestonePanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MilestonePanel.d.ts","sourceRoot":"","sources":["../../src/chat/MilestonePanel.tsx"],"names":[],"mappings":"AAYA,UAAU,mBAAmB;IACzB,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;CAChC;AAED,wBAAgB,cAAc,CAAC,EAC3B,cAAc,EACd,aAAa,EACb,YAAoB,EACpB,gBAAuB,EACvB,OAAe,EACf,eAAe,EAClB,EAAE,mBAAmB,2CA6brB"}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { chatApi } from "@myrjfa/state/dist/lib/actions/chat";
|
|
5
|
+
import { CheckCircle2, Circle, Plus, Trash2, Pencil, MoreVertical, Lock, Unlock, ListTodo, ChevronDown, AlertCircle, ChevronDownIcon, ChevronRightIcon } from "lucide-react";
|
|
6
|
+
import { Button } from "../misc/button";
|
|
7
|
+
import { Input } from "../misc/input";
|
|
8
|
+
import { Textarea } from "../misc/textarea";
|
|
9
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../misc/dropdown-menu";
|
|
10
|
+
import { deleteActions, showAlert } from "../misc/alertdialogclaude";
|
|
11
|
+
export function MilestonePanel({ conversationId, currentUserId, isFullScreen = false, isFeatureEnabled = true, isAdmin = false, onEnableFeature }) {
|
|
12
|
+
const [milestones, setMilestones] = useState([]);
|
|
13
|
+
const [loading, setLoading] = useState(true);
|
|
14
|
+
const [isAdding, setIsAdding] = useState(false);
|
|
15
|
+
const [milestoneId, setMilestoneId] = useState(null);
|
|
16
|
+
const [newTitle, setNewTitle] = useState("");
|
|
17
|
+
const [newSection, setNewSection] = useState("");
|
|
18
|
+
const [newDescription, setNewDescription] = useState("");
|
|
19
|
+
const [newDueDate, setNewDueDate] = useState("");
|
|
20
|
+
const [showMoreOptions, setShowMoreOptions] = useState(false);
|
|
21
|
+
const [sections, setSections] = useState([]);
|
|
22
|
+
const [expandedSections, setExpandedSections] = useState(new Set());
|
|
23
|
+
const [expandedMilestones, setExpandedMilestones] = useState(new Set());
|
|
24
|
+
const [allParticipants, setAllParticipants] = useState([]);
|
|
25
|
+
const fetchMilestones = async () => {
|
|
26
|
+
var _a;
|
|
27
|
+
setLoading(true);
|
|
28
|
+
const data = await chatApi.getMilestones(conversationId);
|
|
29
|
+
const convData = await chatApi.getConversation(conversationId);
|
|
30
|
+
setMilestones(data);
|
|
31
|
+
const participants = ((_a = convData === null || convData === void 0 ? void 0 : convData.participants) === null || _a === void 0 ? void 0 : _a.map((p) => p.userId)) || [];
|
|
32
|
+
setAllParticipants(participants);
|
|
33
|
+
setLoading(false);
|
|
34
|
+
};
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
fetchMilestones();
|
|
37
|
+
}, [conversationId]);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
// Extract unique sections from existing milestones
|
|
40
|
+
const uniqueSections = Array.from(new Set(milestones.map(m => m.section).filter(Boolean)));
|
|
41
|
+
setSections(uniqueSections);
|
|
42
|
+
}, [milestones]);
|
|
43
|
+
const resetForm = () => {
|
|
44
|
+
setNewTitle("");
|
|
45
|
+
setNewSection("");
|
|
46
|
+
setNewDescription("");
|
|
47
|
+
setNewDueDate("");
|
|
48
|
+
setShowMoreOptions(false);
|
|
49
|
+
setIsAdding(false);
|
|
50
|
+
setShowMoreOptions(false);
|
|
51
|
+
setIsAdding(false);
|
|
52
|
+
setMilestoneId(null);
|
|
53
|
+
};
|
|
54
|
+
const toggleSection = (section) => {
|
|
55
|
+
setExpandedSections(prev => {
|
|
56
|
+
const newSet = new Set(prev);
|
|
57
|
+
if (newSet.has(section)) {
|
|
58
|
+
newSet.delete(section);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
newSet.add(section);
|
|
62
|
+
}
|
|
63
|
+
return newSet;
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
const toggleMilestoneExpand = (id) => {
|
|
67
|
+
setExpandedMilestones(prev => {
|
|
68
|
+
const newSet = new Set(prev);
|
|
69
|
+
if (newSet.has(id)) {
|
|
70
|
+
newSet.delete(id);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
newSet.add(id);
|
|
74
|
+
}
|
|
75
|
+
return newSet;
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
const handleCreate = async () => {
|
|
79
|
+
if (!newTitle.trim())
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
const milestoneData = {
|
|
83
|
+
title: newTitle.trim(),
|
|
84
|
+
section: newSection.trim() || "General",
|
|
85
|
+
description: newDescription.trim() || undefined,
|
|
86
|
+
dueDate: newDueDate || undefined
|
|
87
|
+
};
|
|
88
|
+
const milestoneResponse = milestoneId
|
|
89
|
+
? await chatApi.updateMilestone(milestoneId, milestoneData)
|
|
90
|
+
: await chatApi.createMilestone(conversationId, milestoneData);
|
|
91
|
+
if (milestoneResponse.error) {
|
|
92
|
+
console.error("Failed to create milestone:", milestoneResponse.error);
|
|
93
|
+
showAlert(milestoneResponse.error, "Failed to create milestone");
|
|
94
|
+
}
|
|
95
|
+
else if (milestoneResponse.data) {
|
|
96
|
+
await fetchMilestones();
|
|
97
|
+
resetForm();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
console.error("Failed to create milestone:", err);
|
|
102
|
+
showAlert(err, "Failed to create milestone");
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
const handleToggle = async (id) => {
|
|
106
|
+
const response = await chatApi.toggleMilestone(id);
|
|
107
|
+
if (response.error) {
|
|
108
|
+
console.log("error toggling milestone " + id + "Error: " + response.error);
|
|
109
|
+
await showAlert("Error checking/unchecking milestone");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
else if (response.data) {
|
|
113
|
+
setMilestones(prev => prev.map(m => m._id === id ? response.data : m));
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const handleDelete = async (id) => {
|
|
117
|
+
let confirmDelete = true;
|
|
118
|
+
await showAlert("Are you sure you want to delete this milestone?", undefined, deleteActions(() => confirmDelete = false));
|
|
119
|
+
if (confirmDelete) {
|
|
120
|
+
const response = await chatApi.deleteMilestone(id);
|
|
121
|
+
if (response.error) {
|
|
122
|
+
console.log("Unable to delete milestone:" + id + "Error: " + response.error);
|
|
123
|
+
await showAlert("Unable to delete milestone");
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
setMilestones(prev => prev.filter(m => m._id !== id));
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const handleEdit = async (milestone) => {
|
|
130
|
+
var _a, _b, _c;
|
|
131
|
+
setNewTitle(milestone.title);
|
|
132
|
+
setNewSection((_a = milestone.section) !== null && _a !== void 0 ? _a : "");
|
|
133
|
+
setNewDescription((_b = milestone.description) !== null && _b !== void 0 ? _b : "");
|
|
134
|
+
setNewDueDate((_c = milestone.dueDate) !== null && _c !== void 0 ? _c : "");
|
|
135
|
+
setMilestoneId(milestone._id);
|
|
136
|
+
setIsAdding(true);
|
|
137
|
+
};
|
|
138
|
+
// Check if current user has checked this milestone
|
|
139
|
+
const hasUserChecked = (milestone) => {
|
|
140
|
+
return milestone.checks.some(check => check.userId === currentUserId);
|
|
141
|
+
};
|
|
142
|
+
// Get users who haven't checked the milestone
|
|
143
|
+
const getUncheckedUsers = (milestone) => {
|
|
144
|
+
const checkedUserIds = new Set(milestone.checks.map(check => check.userId));
|
|
145
|
+
return allParticipants.filter(userId => !checkedUserIds.has(userId));
|
|
146
|
+
};
|
|
147
|
+
const groupedMilestones = milestones.reduce((acc, m) => {
|
|
148
|
+
const section = m.section || "General";
|
|
149
|
+
if (!acc[section])
|
|
150
|
+
acc[section] = [];
|
|
151
|
+
acc[section].push(m);
|
|
152
|
+
return acc;
|
|
153
|
+
}, {});
|
|
154
|
+
const panelClasses = isFullScreen
|
|
155
|
+
? "w-full h-full flex flex-col"
|
|
156
|
+
: "flex flex-col h-full w-full md:w-80 border-l border-gray-200 dark:border-gray-700";
|
|
157
|
+
return (_jsxs("div", { className: `bg-white dark:bg-gray-900 ${panelClasses}`, children: [_jsx("div", { className: "p-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0", children: _jsx("h3", { className: "font-semibold text-gray-900 dark:text-white", children: "Milestones" }) }), _jsx("div", { className: "flex-1 overflow-y-auto p-4 space-y-4", children: !isFeatureEnabled ? (_jsxs("div", { className: "flex flex-col items-center justify-center h-full py-12 px-6 text-center", children: [_jsx("div", { className: "mb-4 p-4 bg-teal-50 dark:bg-teal-900/20 rounded-full", children: _jsx(ListTodo, { className: "w-12 h-12 text-teal-500" }) }), _jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white mb-2", children: "Milestones Feature" }), _jsx("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-4 max-w-sm", children: "Track group tasks, organize by sections, and monitor progress together. Perfect for trip planning and group coordination." }), isAdmin ? (_jsxs(Button, { onClick: onEnableFeature, className: "bg-gradient-to-r from-teal-500 to-cyan-600 hover:from-teal-600 hover:to-cyan-700 text-white", children: [_jsx(Unlock, { className: "w-4 h-4 mr-2" }), "Enable Feature"] })) : (_jsxs("div", { className: "flex items-center gap-2 text-sm text-gray-500", children: [_jsx(Lock, { className: "w-4 h-4" }), "Ask admin to enable this feature"] }))] })) : loading ? (_jsx("div", { className: "flex justify-center py-8", children: _jsx("div", { className: "animate-spin w-5 h-5 border-2 border-teal-500 border-t-transparent rounded-full" }) })) : milestones.length === 0 && !isAdding ? (_jsx("div", { className: "text-center py-8 text-gray-400 text-sm", children: "No milestones yet" })) : (_jsxs(_Fragment, { children: [isAdding && (_jsxs("div", { className: "p-4 bg-gray-50 dark:bg-gray-800 rounded-lg space-y-3", children: [_jsx(Input, { placeholder: "Section (e.g., Day 1, Transport)", value: newSection, onChange: (e) => setNewSection(e.target.value), list: "section-suggestions", className: "text-sm" }), _jsx("datalist", { id: "section-suggestions", children: sections.map(section => (_jsx("option", { value: section }, section))) }), _jsx(Input, { placeholder: "Milestone title *", value: newTitle, onChange: (e) => setNewTitle(e.target.value), onKeyDown: (e) => e.key === "Enter" && !showMoreOptions && handleCreate(), className: "text-sm font-medium", autoFocus: true }), _jsxs("button", { type: "button", onClick: () => setShowMoreOptions(!showMoreOptions), className: "text-xs text-gray-600 dark:text-gray-400 hover:text-teal-600 dark:hover:text-teal-400 flex items-center gap-1 cursor-pointer", children: [showMoreOptions ? _jsx(ChevronDownIcon, { className: "w-3 h-3" }) : _jsx(ChevronRightIcon, { className: "w-3 h-3" }), " More Options"] }), showMoreOptions && (_jsxs("div", { className: "space-y-2 pl-4 border-l-2 border-teal-200 dark:border-teal-800", children: [_jsx(Textarea, { placeholder: "Description (optional)", value: newDescription, onChange: (e) => setNewDescription(e.target.value), className: "text-sm resize-none", rows: 2 }), _jsxs("div", { children: [_jsx("label", { className: "text-xs text-gray-600 dark:text-gray-400 block mb-1", children: "Due Date (optional)" }), _jsx(Input, { type: "date", value: newDueDate, onChange: (e) => setNewDueDate(e.target.value), className: "text-sm" })] })] })), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { onClick: handleCreate, size: "sm", className: "flex-1 bg-gradient-to-r from-teal-500 to-cyan-600 hover:from-teal-600 hover:to-cyan-700", children: "Add" }), _jsx(Button, { onClick: resetForm, size: "sm", variant: "outline", children: "Cancel" })] })] })), milestones.length > 0 && (Object.entries(groupedMilestones).map(([section, items]) => {
|
|
158
|
+
const isExpanded = expandedSections.has(section);
|
|
159
|
+
const completedCount = items.filter(m => m.isCompleted).length;
|
|
160
|
+
const totalCount = items.length;
|
|
161
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("button", { onClick: () => toggleSection(section), className: "w-full flex items-center justify-between p-3 bg-gradient-to-r from-teal-50 to-cyan-50 dark:from-teal-900/20 dark:to-cyan-900/20 hover:from-teal-100 hover:to-cyan-100 dark:hover:from-teal-900/30 dark:hover:to-cyan-900/30 rounded-lg border border-teal-100 dark:border-teal-800 transition-colors", children: [_jsxs("div", { className: "flex items-center gap-3 flex-1 text-left", children: [_jsx(ChevronDown, { className: `w-4 h-4 text-teal-600 dark:text-teal-400 transition-transform flex-shrink-0 ${isExpanded ? "" : "-rotate-90"}` }), _jsx("h4", { className: "text-sm font-semibold text-teal-900 dark:text-teal-100 uppercase tracking-widest", children: section })] }), _jsxs("span", { className: "text-xs font-semibold text-teal-700 dark:text-teal-300 bg-white dark:bg-gray-800 px-2.5 py-1 rounded-full flex-shrink-0", children: [completedCount, "/", totalCount] })] }), isExpanded && (_jsx("div", { className: "space-y-2 pl-4", children: items.map((milestone) => {
|
|
162
|
+
const userChecked = hasUserChecked(milestone);
|
|
163
|
+
const uncheckedUsers = getUncheckedUsers(milestone);
|
|
164
|
+
const isExpanded = expandedMilestones.has(milestone._id);
|
|
165
|
+
return (_jsxs("div", { className: "space-y-0", children: [_jsxs("button", { onClick: () => toggleMilestoneExpand(milestone._id), className: "w-full flex items-start gap-3 p-3 bg-gray-50 dark:bg-gray-800/30 hover:bg-gray-100 dark:hover:bg-gray-800/50 rounded-lg border border-gray-100 dark:border-gray-800 transition-colors group text-left", children: [_jsx("button", { onClick: (e) => {
|
|
166
|
+
e.stopPropagation();
|
|
167
|
+
handleToggle(milestone._id);
|
|
168
|
+
}, className: `mt-0.5 transition-colors flex-shrink-0 ${userChecked ? "text-green-500" : "text-gray-300 hover:text-teal-400"}`, children: userChecked ? _jsx(CheckCircle2, { className: "w-5 h-5" }) : _jsx(Circle, { className: "w-5 h-5" }) }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: `text-sm font-medium transition-all ${userChecked ? "text-gray-400 line-through" : "text-gray-700 dark:text-gray-200"}`, children: milestone.title }), milestone.dueDate && (_jsxs("p", { className: "text-[10px] text-gray-500 dark:text-gray-400 mt-1", children: ["Due: ", new Date(milestone.dueDate).toLocaleDateString()] }))] }), _jsxs("div", { className: "flex items-center gap-2 flex-shrink-0", children: [_jsxs("span", { className: `text-xs font-semibold px-2 py-1 rounded-full ${milestone.isCompleted
|
|
169
|
+
? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
|
|
170
|
+
: "bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300"}`, children: [milestone.checks.length, "/", allParticipants.length] }), _jsx(ChevronDown, { className: `w-4 h-4 text-gray-400 transition-transform flex-shrink-0 ${isExpanded ? "" : "-rotate-90"}` })] }), (milestone.createdBy === currentUserId || isAdmin) && (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("button", { onClick: (e) => e.stopPropagation(), className: "p-1 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-md transition-all flex-shrink-0", children: _jsx(MoreVertical, { className: "w-4 h-4 text-gray-400" }) }) }), _jsxs(DropdownMenuContent, { align: "end", children: [_jsxs(DropdownMenuItem, { onClick: () => handleEdit(milestone), children: [_jsx(Pencil, { className: "w-4 h-4 mr-2" }), "Edit"] }), _jsxs(DropdownMenuItem, { className: "text-red-500", onClick: () => handleDelete(milestone._id), children: [_jsx(Trash2, { className: "w-4 h-4 mr-2" }), "Delete"] })] })] }))] }), isExpanded && (_jsxs("div", { className: "ml-8 bg-white dark:bg-gray-700/30 p-3 rounded-lg border border-gray-200 dark:border-gray-700 space-y-2", children: [milestone.description && (_jsxs("div", { children: [_jsx("p", { className: "text-[10px] font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-1", children: "Description" }), _jsx("p", { className: "text-xs text-gray-700 dark:text-gray-300", children: milestone.description })] })), uncheckedUsers.length > 0 && (_jsxs("div", { children: [_jsxs("p", { className: "text-[10px] font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-1", children: ["Pending (", uncheckedUsers.length, ")"] }), _jsx("div", { className: "space-y-1", children: uncheckedUsers.map((userId) => (_jsxs("div", { className: "flex items-center gap-2 p-1.5 bg-yellow-50 dark:bg-yellow-900/20 rounded border border-yellow-100 dark:border-yellow-800/30", children: [_jsx(AlertCircle, { className: "w-3 h-3 text-yellow-600 dark:text-yellow-500 flex-shrink-0" }), _jsx("span", { className: "text-xs text-yellow-700 dark:text-yellow-300", children: userId })] }, userId))) })] })), milestone.isCompleted && (_jsxs("div", { className: "flex items-center gap-2 p-2 bg-green-50 dark:bg-green-900/20 rounded border border-green-200 dark:border-green-800/30", children: [_jsx(CheckCircle2, { className: "w-4 h-4 text-green-600 dark:text-green-400 flex-shrink-0" }), _jsx("p", { className: "text-xs text-green-700 dark:text-green-400 font-medium", children: "All participants have checked this milestone" })] }))] }))] }, milestone._id));
|
|
171
|
+
}) }))] }, section));
|
|
172
|
+
}))] })) }), isFeatureEnabled && !isAdding && (_jsx("div", { className: "p-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0", children: _jsxs(Button, { onClick: () => setIsAdding(true), className: "w-full bg-gradient-to-r from-teal-500 to-cyan-600 hover:from-teal-600 hover:to-cyan-700 text-white rounded-lg flex items-center justify-center gap-2", children: [_jsx(Plus, { className: "w-4 h-4" }), "Add Milestone"] }) }))] }));
|
|
173
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ExpenseSplit } from "@myrjfa/state";
|
|
2
|
+
import { Dispatch, SetStateAction } from "react";
|
|
3
|
+
interface ParticipantSelectorProps {
|
|
4
|
+
participants: string[];
|
|
5
|
+
onChange: (participants: string[]) => void;
|
|
6
|
+
onSplitsChange: Dispatch<SetStateAction<ExpenseSplit[]>>;
|
|
7
|
+
availableUsers: string[];
|
|
8
|
+
}
|
|
9
|
+
export declare function ParticipantSelector({ participants, onChange, onSplitsChange, availableUsers }: ParticipantSelectorProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=ParticipantSelector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ParticipantSelector.d.ts","sourceRoot":"","sources":["../../../src/chat/components/ParticipantSelector.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,OAAO,CAAC;AAEjD,UAAU,wBAAwB;IAC9B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAC3C,cAAc,EAAE,QAAQ,CAAC,cAAc,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC;IACzD,cAAc,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,wBAAgB,mBAAmB,CAAC,EAChC,YAAY,EACZ,QAAQ,EACR,cAAc,EACd,cAAc,EACjB,EAAE,wBAAwB,2CA2D1B"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { X } from "lucide-react";
|
|
4
|
+
export function ParticipantSelector({ participants, onChange, onSplitsChange, availableUsers }) {
|
|
5
|
+
const addParticipant = (userId) => {
|
|
6
|
+
if (!participants.includes(userId)) {
|
|
7
|
+
onChange([...participants, userId]);
|
|
8
|
+
onSplitsChange(prev => [...prev, { userId }]);
|
|
9
|
+
}
|
|
10
|
+
};
|
|
11
|
+
const removeParticipant = (userId) => {
|
|
12
|
+
onChange(participants.filter(p => p !== userId));
|
|
13
|
+
onSplitsChange(prev => prev.filter(s => s.userId !== userId));
|
|
14
|
+
};
|
|
15
|
+
const unselectedUsers = availableUsers.filter(u => !participants.includes(u));
|
|
16
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider", children: "Split Among" }), _jsx("div", { className: "flex flex-wrap gap-2", children: participants.length === 0 ? (_jsx("p", { className: "text-xs text-gray-500", children: "Select participants..." })) : (participants.map(userId => (_jsxs("div", { className: "flex items-center gap-1 bg-teal-100 dark:bg-teal-900/30 text-teal-700 dark:text-teal-300 rounded-full px-3 py-1 text-xs", children: [userId, _jsx("button", { onClick: () => removeParticipant(userId), className: "hover:text-teal-900 dark:hover:text-teal-100 transition-colors", children: _jsx(X, { className: "w-3 h-3" }) })] }, userId)))) }), unselectedUsers.length > 0 && (_jsx("div", { className: "flex flex-wrap gap-1", children: unselectedUsers.map(userId => (_jsxs("button", { onClick: () => addParticipant(userId), className: "text-xs px-2 py-1 rounded-full border border-gray-300 dark:border-gray-600 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors", children: ["+ ", userId] }, userId))) }))] }));
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SplitType, ExpenseSplit } from "@myrjfa/state/dist/lib/models/chat";
|
|
2
|
+
interface SplitInputsProps {
|
|
3
|
+
splitType: SplitType;
|
|
4
|
+
totalAmount: number;
|
|
5
|
+
participants: string[];
|
|
6
|
+
splits: ExpenseSplit[];
|
|
7
|
+
onChange: (splits: ExpenseSplit[]) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function SplitInputs({ splitType, totalAmount, participants, splits, onChange }: SplitInputsProps): import("react/jsx-runtime").JSX.Element | null;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=SplitInputs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SplitInputs.d.ts","sourceRoot":"","sources":["../../../src/chat/components/SplitInputs.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAGlF,UAAU,gBAAgB;IACtB,SAAS,EAAE,SAAS,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;CAC9C;AAED,wBAAgB,WAAW,CAAC,EACxB,SAAS,EACT,WAAW,EACX,YAAY,EACZ,MAAM,EACN,QAAQ,EACX,EAAE,gBAAgB,kDA8KlB"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from "react";
|
|
4
|
+
import { ChevronDown } from "lucide-react";
|
|
5
|
+
export function SplitInputs({ splitType, totalAmount, participants, splits, onChange }) {
|
|
6
|
+
const [isExpanded, setIsExpanded] = useState(splitType !== "equal");
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
setIsExpanded(splitType !== "equal");
|
|
9
|
+
}, [splitType]);
|
|
10
|
+
if (splitType === "equal") {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
const handleInputChange = (userId, value, field) => {
|
|
14
|
+
const numValue = parseFloat(value) || 0;
|
|
15
|
+
const updatedSplits = splits.map(s => {
|
|
16
|
+
if (s.userId === userId) {
|
|
17
|
+
return { ...s, [field]: numValue, amount: field === "amount" ? numValue : s.amount };
|
|
18
|
+
}
|
|
19
|
+
return s;
|
|
20
|
+
});
|
|
21
|
+
// Auto-calculate amounts for percentage and shares
|
|
22
|
+
if (field === "percentage") {
|
|
23
|
+
updatedSplits.forEach(s => {
|
|
24
|
+
if (s.userId === userId) {
|
|
25
|
+
s.amount = (numValue / 100) * totalAmount;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
else if (field === "shares") {
|
|
30
|
+
const totalShares = updatedSplits.reduce((sum, s) => sum + (s.shares || 0), 0);
|
|
31
|
+
updatedSplits.forEach(s => {
|
|
32
|
+
s.amount = (s.shares && s.shares > 0) ? (s.shares / totalShares) * totalAmount : 0;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
onChange(updatedSplits);
|
|
36
|
+
};
|
|
37
|
+
const getInputLabel = () => {
|
|
38
|
+
switch (splitType) {
|
|
39
|
+
case "exact":
|
|
40
|
+
return "Amount";
|
|
41
|
+
case "percentage":
|
|
42
|
+
return "Percentage";
|
|
43
|
+
case "shares":
|
|
44
|
+
return "Shares";
|
|
45
|
+
default:
|
|
46
|
+
return "Amount";
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const validateSplits = () => {
|
|
50
|
+
if (splitType === "percentage") {
|
|
51
|
+
const total = splits.reduce((sum, s) => sum + (s.percentage || 0), 0);
|
|
52
|
+
return total === 100;
|
|
53
|
+
}
|
|
54
|
+
else if (splitType === "exact") {
|
|
55
|
+
const total = splits.reduce((sum, s) => sum + (s.amount || 0), 0);
|
|
56
|
+
return Math.abs(total - totalAmount) < 0.01;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
};
|
|
60
|
+
const isValid = getInputLabel();
|
|
61
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsxs("button", { onClick: () => setIsExpanded(!isExpanded), className: "w-full flex items-center justify-between p-2 rounded-lg border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors", children: [_jsx("span", { className: "text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider", children: "Split Details" }), _jsx(ChevronDown, { className: `w-4 h-4 text-gray-400 transition-transform ${isExpanded ? "rotate-180" : ""}` })] }), isExpanded && (_jsx("div", { className: "bg-gray-50 dark:bg-gray-800/30 p-3 rounded-lg border border-gray-200 dark:border-gray-700 space-y-2", children: participants.length === 0 ? (_jsx("p", { className: "text-xs text-gray-500 text-center py-2", children: "Add participants to split expense" })) : (_jsxs(_Fragment, { children: [splits.map((split, idx) => {
|
|
62
|
+
var _a;
|
|
63
|
+
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("label", { className: "text-xs text-gray-600 dark:text-gray-400 flex-1 truncate font-medium", children: split.userId }), _jsxs("div", { className: "flex items-center gap-1 flex-1", children: [_jsx("input", { type: "number", step: "1", value: splitType === "exact"
|
|
64
|
+
? split.amount || ""
|
|
65
|
+
: splitType === "percentage"
|
|
66
|
+
? split.percentage || ""
|
|
67
|
+
: split.shares || "", onChange: (e) => handleInputChange(split.userId, e.target.value, splitType === "exact"
|
|
68
|
+
? "amount"
|
|
69
|
+
: splitType === "percentage"
|
|
70
|
+
? "percentage"
|
|
71
|
+
: "shares"), placeholder: "0", className: "w-20 bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded p-1 text-xs text-gray-900 dark:text-white outline-none focus:border-teal-500" }), _jsx("span", { className: "text-[10px] text-gray-500 min-w-fit", children: splitType === "percentage"
|
|
72
|
+
? "%"
|
|
73
|
+
: splitType === "shares"
|
|
74
|
+
? "shares"
|
|
75
|
+
: `₹${((_a = split.amount) === null || _a === void 0 ? void 0 : _a.toFixed(2)) || "0.00"}` })] })] }, split.userId));
|
|
76
|
+
}), splitType === "percentage" && (_jsx("div", { className: "text-[10px] text-gray-600 dark:text-gray-400 pt-2 border-t border-gray-200 dark:border-gray-600", children: _jsxs("p", { children: ["Total:", " ", _jsxs("span", { className: Math.abs(splits.reduce((sum, s) => sum + (s.percentage || 0), 0) -
|
|
77
|
+
100) < 0.01
|
|
78
|
+
? "text-green-600"
|
|
79
|
+
: "text-red-500", children: [splits.reduce((sum, s) => sum + (s.percentage || 0), 0).toFixed(1), "%"] })] }) })), splitType === "exact" && (_jsx("div", { className: "text-[10px] text-gray-600 dark:text-gray-400 pt-2 border-t border-gray-200 dark:border-gray-600", children: _jsxs("p", { children: ["Total:", " ", _jsxs("span", { className: Math.abs(splits.reduce((sum, s) => sum + (s.amount || 0), 0) -
|
|
80
|
+
totalAmount) < 0.01
|
|
81
|
+
? "text-green-600"
|
|
82
|
+
: "text-red-500", children: ["\u20B9", splits.reduce((sum, s) => sum + (s.amount || 0), 0).toFixed(2)] })] }) }))] })) }))] }));
|
|
83
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SplitType } from "@myrjfa/state/dist/lib/models/chat";
|
|
2
|
+
interface SplitTypeSelectorProps {
|
|
3
|
+
value: SplitType;
|
|
4
|
+
onChange: (type: SplitType) => void;
|
|
5
|
+
}
|
|
6
|
+
export declare function SplitTypeSelector({ value, onChange }: SplitTypeSelectorProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export {};
|
|
8
|
+
//# sourceMappingURL=SplitTypeSelector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SplitTypeSelector.d.ts","sourceRoot":"","sources":["../../../src/chat/components/SplitTypeSelector.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oCAAoC,CAAC;AAGpE,UAAU,sBAAsB;IAC5B,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAC;CACvC;AASD,wBAAgB,iBAAiB,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,sBAAsB,2CA4B5E"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
const SPLIT_TYPES = [
|
|
4
|
+
{ value: "equal", label: "Equal", description: "Split evenly among all" },
|
|
5
|
+
{ value: "exact", label: "Exact", description: "Specify exact amounts" },
|
|
6
|
+
{ value: "percentage", label: "Percentage", description: "Split by percentages" },
|
|
7
|
+
{ value: "shares", label: "Shares", description: "Split by number of shares" }
|
|
8
|
+
];
|
|
9
|
+
export function SplitTypeSelector({ value, onChange }) {
|
|
10
|
+
return (_jsxs("div", { className: "space-y-2", children: [_jsx("label", { className: "text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wider", children: "Split Type" }), _jsx("div", { className: "grid grid-cols-2 gap-2", children: SPLIT_TYPES.map((type) => (_jsxs("button", { onClick: () => onChange(type.value), className: `p-2 rounded-lg border-2 transition-all text-left ${value === type.value
|
|
11
|
+
? "border-teal-500 bg-teal-50 dark:bg-teal-900/20"
|
|
12
|
+
: "border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600"}`, children: [_jsx("p", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: type.label }), _jsx("p", { className: "text-[10px] text-gray-500 dark:text-gray-400", children: type.description })] }, type.value))) })] }));
|
|
13
|
+
}
|