From f1a1db5ff60bde868d75c3be15d4263b40e6fd5a Mon Sep 17 00:00:00 2001 From: caitlin Date: Wed, 17 Jun 2026 17:35:54 +0200 Subject: [PATCH 1/3] This is the template for us to work on. --- package-lock.json | 40 +------------------ .../dashboard/lead-status-chart.tsx | 9 +++-- .../dashboard/leads-per-month-chart.tsx | 13 +++--- src/components/dashboard/stat-card.tsx | 2 +- 4 files changed, 14 insertions(+), 50 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf0da78..86e3cf3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -108,14 +108,12 @@ "node_modules/@emoji-mart/data": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emoji-mart/data/-/data-1.2.1.tgz", - "integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==", - "license": "MIT" + "integrity": "sha512-no2pQMWiBy6gpBEiqGeU77/bFejDqUTRY7KX+0+iur13op3bqUsXdnwoZs6Xb1zbv0gAj5VvS1PWoUUckSr5Dw==" }, "node_modules/@emoji-mart/react": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@emoji-mart/react/-/react-1.1.1.tgz", "integrity": "sha512-NMlFNeWgv1//uPsvLxvGQoIerPuVdXwK/EUek8OOkJ6wVOWPUizRBJU0hDqWZCOROVpfBgCemaC3m6jDOXi03g==", - "license": "MIT", "peerDependencies": { "emoji-mart": "^5.2", "react": "^16.8 || ^17 || ^18" @@ -441,9 +439,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -460,9 +455,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -479,9 +471,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -498,9 +487,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -517,9 +503,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -536,9 +519,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -555,9 +535,6 @@ "cpu": [ "arm" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -580,9 +557,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -605,9 +579,6 @@ "cpu": [ "s390x" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -630,9 +601,6 @@ "cpu": [ "x64" ], - "libc": [ - "glibc" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -655,9 +623,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ @@ -680,9 +645,6 @@ "cpu": [ "x64" ], - "libc": [ - "musl" - ], "license": "Apache-2.0", "optional": true, "os": [ diff --git a/src/components/dashboard/lead-status-chart.tsx b/src/components/dashboard/lead-status-chart.tsx index 1bd98d4..12ec99f 100644 --- a/src/components/dashboard/lead-status-chart.tsx +++ b/src/components/dashboard/lead-status-chart.tsx @@ -51,15 +51,16 @@ export function LeadStatusChart({ data }: LeadStatusChartProps) { initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3, delay: 0.2 }} + className="h-full" > - + Lead Status - -
+ +
{/* Donut */} - + {segs.map((s, i) => ( diff --git a/src/components/dashboard/leads-per-month-chart.tsx b/src/components/dashboard/leads-per-month-chart.tsx index dcdf708..1be8046 100644 --- a/src/components/dashboard/leads-per-month-chart.tsx +++ b/src/components/dashboard/leads-per-month-chart.tsx @@ -22,8 +22,8 @@ export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) { const [hovered, setHovered] = useState(null) const width = 880 - const height = 380 - const padding = { top: 28, right: 24, bottom: 44, left: 44 } + const height = 620 + const padding = { top: 128, right: 24, bottom: 48, left: 44 } const chartW = width - padding.left - padding.right const chartH = height - padding.top - padding.bottom @@ -55,8 +55,9 @@ export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) { initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3, delay: 0.3 }} + className="h-full" > - +
@@ -77,12 +78,12 @@ export function LeadsPerMonthChart({ data }: LeadsPerMonthChartProps) {
- -
+ +
diff --git a/src/components/dashboard/stat-card.tsx b/src/components/dashboard/stat-card.tsx index b2a9eb8..2854439 100644 --- a/src/components/dashboard/stat-card.tsx +++ b/src/components/dashboard/stat-card.tsx @@ -32,7 +32,7 @@ export function StatCard({ title, value, icon: Icon, variant = "default", descri animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.3, delay: index * 0.05 }} > - +
From 7a42e3c41c4c3fa07c4623f9ee5db0d3e3c17411 Mon Sep 17 00:00:00 2001 From: caitlin Date: Thu, 18 Jun 2026 09:38:06 +0200 Subject: [PATCH 2/3] I fixed the runtime error that appeared. I fixed the size error regarding both graphs. --- src/components/layout/topbar.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/layout/topbar.tsx b/src/components/layout/topbar.tsx index 119d69f..3a9e7d5 100644 --- a/src/components/layout/topbar.tsx +++ b/src/components/layout/topbar.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { useRouter } from "next/navigation" import { useTheme } from "next-themes" import { cn } from "@/lib/utils" @@ -36,7 +36,10 @@ export function Topbar({ onMenuClick }: TopbarProps) { const router = useRouter() const { theme, setTheme } = useTheme() const { user } = useUser() + const [mounted, setMounted] = useState(false) const [searchOpen, setSearchOpen] = useState(false) + + useEffect(() => { setMounted(true) }, []) const initials = user.name.split(" ").map((n) => n[0]).join("") return ( @@ -73,7 +76,7 @@ export function Topbar({ onMenuClick }: TopbarProps) { onClick={() => setTheme(theme === "dark" ? "light" : "dark")} className="text-muted-foreground" > - {theme === "dark" ? : } + {mounted ? (theme === "dark" ? : ) :
} {/* Notifications */} From 8cb49167f47f8a9260c45c0384842f2aa806ef1c Mon Sep 17 00:00:00 2001 From: Ace Date: Thu, 18 Jun 2026 12:25:28 +0200 Subject: [PATCH 3/3] added Voice, blueticks, fixed messages, fixed search conversation, added scroll functionality, fixed file uploads & download added in screenshot paste function too, changed back to the orignal emoji pack --- src/app/(dashboard)/chats/page.tsx | 617 ++++++++++++++++++++++++----- src/app/globals.css | 3 + src/components/ui/scroll-area.tsx | 8 +- 3 files changed, 525 insertions(+), 103 deletions(-) diff --git a/src/app/(dashboard)/chats/page.tsx b/src/app/(dashboard)/chats/page.tsx index d8eadf9..2095f61 100644 --- a/src/app/(dashboard)/chats/page.tsx +++ b/src/app/(dashboard)/chats/page.tsx @@ -17,7 +17,8 @@ import { import { conversations as conversationsData } from "@/data/chats" import { Search, Send, Phone, Video, MoreHorizontal, Paperclip, - Smile, Flag, Ban, Trash2, Image, File, X, + Smile, Flag, Ban, Trash2, Image, FileIcon, X, Mic, Square, Play, Pause, Check, CheckCheck, + CornerDownRight, Forward, Pencil, Download, } from "lucide-react" import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, @@ -31,6 +32,65 @@ import { toast } from "sonner" import data from "@emoji-mart/data" import Picker from "@emoji-mart/react" +function VoiceMessagePlayer({ src, initialDuration }: { src: string; initialDuration: number }) { + const [playing, setPlaying] = useState(false) + const [current, setCurrent] = useState(0) + const [duration, setDuration] = useState(initialDuration) + const audioRef = useRef(null) + const animRef = useRef(0) + + useEffect(() => { + const audio = audioRef.current + if (!audio) return + const onLoaded = () => { + if (isFinite(audio.duration)) setDuration(audio.duration) + } + const onEnded = () => { setPlaying(false); setCurrent(0) } + audio.addEventListener("loadedmetadata", onLoaded) + audio.addEventListener("ended", onEnded) + return () => { + audio.removeEventListener("loadedmetadata", onLoaded) + audio.removeEventListener("ended", onEnded) + } + }, [src]) + + useEffect(() => { + if (!playing) { cancelAnimationFrame(animRef.current); return } + const tick = () => { + if (audioRef.current) setCurrent(audioRef.current.currentTime) + animRef.current = requestAnimationFrame(tick) + } + animRef.current = requestAnimationFrame(tick) + return () => cancelAnimationFrame(animRef.current) + }, [playing]) + + const toggle = () => { + if (!audioRef.current) return + if (playing) { audioRef.current.pause(); setPlaying(false) } + else { audioRef.current.play(); setPlaying(true) } + } + + const displayDuration = isFinite(duration) && duration > 0 ? duration : initialDuration + const pct = displayDuration > 0 ? (current / displayDuration) * 100 : 0 + const fmt = (s: number) => { + const secs = isFinite(s) ? Math.max(0, Math.floor(s)) : 0 + return `${Math.floor(secs / 60)}:${(secs % 60).toString().padStart(2, "0")}` + } + + return ( +
+
- {conversations.map((conv) => { + {filteredConversations.map((conv) => { const person = otherParticipant(conv) const isActive = conv.id === activeChat return ( @@ -180,6 +430,9 @@ export default function ChatsPage() { ) })} + {filteredConversations.length === 0 && searchQuery && ( +
No conversations found
+ )}
@@ -206,16 +459,10 @@ export default function ChatsPage() {
- - @@ -234,10 +481,7 @@ export default function ChatsPage() { Block - toast.info("Conversation deleted")} - > + toast.info("Conversation deleted")}> Delete @@ -246,12 +490,13 @@ export default function ChatsPage() {
{/* Messages */} - -
+
+
{conversation.messages.map((msg) => { const isMe = msg.senderId === "user1" + const voiceUrl = voiceMessages.get(msg.id) return ( -
+
{isMe ? : null} @@ -261,92 +506,208 @@ export default function ChatsPage() {
- {msg.content} + {!isMe && ( + + + + + + setReplyingTo(msg)}> + Reply + + { setForwardMessage(msg); setForwardDialogOpen(true) }}> + Forward + + + + )} + {isMe && ( + + + + + + { setEditingMessage(msg); setMessageInput(msg.content) }}> + Edit + + setReplyingTo(msg)}> + Reply + + { setForwardMessage(msg); setForwardDialogOpen(true) }}> + Forward + + + handleDeleteMessage(msg.id)}> + Delete + + + + )} + {(() => { + const replyInfo = replyMap.get(msg.id) + return ( + <> + {replyInfo && ( +
+

{replyInfo.repliedToSender}

+

{replyInfo.repliedToContent}

+
+ )} + {voiceUrl ? ( +
+ + {msg.content && ( +

{msg.content}

+ )} +
+ ) : ( + <> + {(() => { + const files = messageAttachments.get(msg.id) + return files && files.length > 0 ? ( +
+ {files.map((file, i) => + file.type.startsWith("image/") ? ( +
+ {file.name} window.open(file.url, "_blank")} /> + + + +
+ ) : ( + + + {file.name} + + + ) + )} +
+ ) : null + })()} + {isOnlyEmoji(msg.content) ? ( + {msg.content.trim()} + ) : ( + msg.content + )} + + )} + + ) + })()}
- {msg.timestamp} + + {msg.timestamp} + {isMe && ( + (() => { + const status = messageStatus.get(msg.id) + if (status === "read") return + if (status === "delivered") return + return + })() + )} +
) })} +
- +
{/* Input */}
{attachments.length > 0 && (
{attachments.map((file, i) => ( -
+
{isImageFile(file) ? ( ) : ( - + )} {file.name} {formatFileSize(file.size)} -
))}
)} + {/* Reply bar */} + {replyingTo && ( +
+
+

+ Replying to {replyingTo.senderId === "user1" ? "yourself" : otherParticipant(conversation).name} +

+

{replyingTo.content}

+
+ +
+ )} + {editingMessage && ( +
+ + Editing message + +
+ )}
- -
- setMessageInput(e.target.value)} - placeholder="Type a message..." - className="h-9 w-full pr-9" - /> -
- - {showEmojiPicker && ( -
- + {isRecording ? ( +
+ + {formatDuration(recordingDuration)} +
+ + + +
+ ) : ( + <> +