Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions app/api/conversations/[id]/presence/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,15 @@ export async function PATCH(
}

const { id: conversationId } = await params;
const body = await req.json().catch(() => ({}));
const { markRead } = body as { markRead?: boolean };
const { mark_read } = await req.json();

const timestamp = new Date();

const data: { lastSeenAt: Date; lastReadAt?: Date } = {
lastSeenAt: timestamp,
const data: { last_seen_at: Date; last_read_at?: Date } = {
last_seen_at: timestamp,
};
if (markRead) {
data.lastReadAt = timestamp;
if (mark_read) {
data.last_read_at = timestamp;
}

await prisma.conversationParticipant.updateMany({
Expand Down
10 changes: 5 additions & 5 deletions app/api/conversations/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ export async function POST(req: Request) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { otherUserId, otherUserEmail } = await req.json();
const { other_user_id, other_user_email } = await req.json();

if (!otherUserId) {
if (!other_user_id) {
return NextResponse.json(
{ error: "otherUserId is required." },
{ error: "other_user_id is required." },
{ status: 400 },
);
}
Expand All @@ -71,8 +71,8 @@ export async function POST(req: Request) {
last_read_at: timestamp,
},
{
user_id: otherUserId,
email: otherUserEmail ?? "",
user_id: other_user_id,
email: other_user_email ?? "",
last_seen_at: EPOCH,
last_read_at: EPOCH,
},
Expand Down
14 changes: 7 additions & 7 deletions app/api/messages/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,10 @@ export async function POST(req: Request) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { conversationId, text, attachments } = await req.json();
const { conversation_id, text, attachments } = await req.json();

if (
!conversationId ||
!conversation_id ||
(!text?.trim() && (!attachments || attachments.length === 0))
) {
return NextResponse.json(
Expand All @@ -73,7 +73,7 @@ export async function POST(req: Request) {
const participant = await prisma.conversationParticipant.findUnique({
where: {
conversation_id_user_id: {
conversation_id: conversationId,
conversation_id,
user_id: session.user.id,
},
},
Expand All @@ -85,7 +85,7 @@ export async function POST(req: Request) {

const message = await prisma.message.create({
data: {
conversation_id: conversationId,
conversation_id,
sender_id: session.user.id,
text: text?.trim() ?? "",
attachments: attachments ?? [],
Expand All @@ -101,11 +101,11 @@ export async function POST(req: Request) {
created_at: message.created_at.toISOString(),
};

emitter.emit(`chat:${conversationId}`, { type: "message", data: payload });
emitter.emit(`chat:${conversation_id}`, { type: "message", data: payload });

const participants = await prisma.conversationParticipant.findMany({
where: {
conversation_id: conversationId,
conversation_id,
user_id: { not: session.user.id },
},
select: { user_id: true },
Expand All @@ -114,7 +114,7 @@ export async function POST(req: Request) {
for (const p of participants) {
emitter.emit(`user:${p.user_id}`, {
type: "new_message",
data: { conversation_id: conversationId, sender_id: session.user.id },
data: { conversation_id, sender_id: session.user.id },
});
}

Expand Down
59 changes: 42 additions & 17 deletions app/api/wakatime/sync/route.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,55 @@
import { NextResponse } from "next/server";
import { getCurrentUser } from "@/app/lib/auth/user";
import {
saveWakatimeApiKey,
syncWakatimeData,
validateWakatimeApiKey,
} from "@/app/lib/wakatime/sync";
import { auth } from "@/app/lib/auth";

export async function GET(request: Request) {
const { user } = await getCurrentUser();
const { searchParams } = new URL(request.url);
const apiKey = searchParams.get("apiKey") || "";
const saveOnly =
searchParams.get("saveOnly") === "1" ||
searchParams.get("saveOnly") === "true";
export async function GET(req: Request) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const validationError = validateWakatimeApiKey(apiKey);
if (validationError) {
return NextResponse.json({ error: validationError }, { status: 400 });
const result = await syncWakatimeData({
userId: session.user.id,
incomingApiKey: "",
storedApiKey: session.user.wakatime_api_key ?? undefined,
});

if (!result.success && result.status !== 200) {
return NextResponse.json(
{ error: result.error },
{ status: result.status },
);
}

if (!user) {
return NextResponse.json({
success: result.success,
data: result.data,
error: result.error,
});
}

export async function POST(req: Request) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

if (saveOnly) {
const result = await saveWakatimeApiKey({ userId: user.id, apiKey });
const { api_key, save_only } = await req.json();

const validationError = validateWakatimeApiKey(api_key);
if (validationError) {
return NextResponse.json({ error: validationError }, { status: 400 });
}

if (save_only) {
const result = await saveWakatimeApiKey({
userId: session.user.id,
apiKey: api_key,
});

if (!result.success) {
return NextResponse.json(
Expand All @@ -37,9 +62,9 @@ export async function GET(request: Request) {
}

const result = await syncWakatimeData({
userId: user.id,
incomingApiKey: apiKey,
storedApiKey: user.wakatime_api_key,
userId: session.user.id,
incomingApiKey: api_key,
storedApiKey: session.user.wakatime_api_key,
});

if (!result.success && result.status !== 200) {
Expand Down
39 changes: 15 additions & 24 deletions app/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -302,19 +302,18 @@ export default function Chat({ user }: { user: ChatUserShape }) {
: undefined;

const activeLabel = isGlobalActive
? "Global Chat"
? "Global"
: activeOtherUser?.email?.split("@")[0] || "Unknown";

const activeSublabel = isGlobalActive
? "Public Channel"
? "Worldwide"
: activeOtherUserOnline
? "Online"
: "Offline";

const activeSublabelClass =
activeOtherUserOnline || isGlobalActive
? "text-emerald-600"
: "text-gray-500";
const activeSublabelClass = activeOtherUserOnline
? "text-emerald-600"
: "text-gray-500";

const typingIndicatorText = activeTypingState
? isGlobalActive
Expand Down Expand Up @@ -355,15 +354,15 @@ export default function Chat({ user }: { user: ChatUserShape }) {
attachments={allMediaAttachments}
onChange={setMediaViewer}
/>
<div className="flex h-screen w-full bg-transparent text-gray-900 overflow-hidden relative">
<div className="flex h-220 sm:h-screen w-full bg-transparent text-gray-900 overflow-hidden relative">
{/* Left Sidebar */}
<div
className={`w-full md:w-[300px] flex-shrink-0 border-r border-gray-200 flex flex-col bg-white md:bg-transparent z-20 absolute md:relative h-full transition-transform duration-300 ${conversationId ? "-translate-x-full md:translate-x-0" : "translate-x-0"}`}
className={`w-full md:w-[300px] flex-shrink-0 border-r border-gray-200 flex flex-col bg-white z-20 absolute md:relative h-full transition-transform duration-300 ${conversationId ? "-translate-x-full md:translate-x-0" : "translate-x-0"}`}
>
<div className="p-5 border-b border-gray-200">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-gray-700 tracking-tight">
Message category
Messages
</h2>
<button
onClick={() => setShowModal(true)}
Expand Down Expand Up @@ -492,7 +491,7 @@ export default function Chat({ user }: { user: ChatUserShape }) {
{conversationId ? (
<>
{/* Header */}
<div className="h-[72px] flex items-center justify-between px-4 sm:px-6 border-b border-gray-200 bg-white/[0.01] z-10 flex-shrink-0">
<div className="h-[72px] flex items-center justify-between px-4 sm:px-6 border-b border-gray-200 bg-white z-10 flex-shrink-0">
<div className="flex items-center gap-2 sm:gap-3.5">
<button
onClick={() => setConversationId(null)}
Expand All @@ -503,16 +502,6 @@ export default function Chat({ user }: { user: ChatUserShape }) {
className="w-3.5 h-3.5"
/>
</button>
<div className="relative">
<div
className={`flex justify-center items-center w-11 h-11 rounded-full text-[16px] font-bold shadow-sm ${isGlobalActive ? "bg-blue-500/15 text-blue-600 border border-blue-500/30" : "bg-neutral-800 text-gray-700 border border-gray-200"}`}
>
{activeInitials}
</div>
{!isGlobalActive && activeOtherUserOnline && (
<div className="absolute bottom-0.5 right-0.5 w-3 h-3 bg-emerald-400 border-[2px] border-transparent rounded-full"></div>
)}
</div>
<div>
<h2 className="text-[16px] font-bold text-gray-700 leading-tight">
{activeLabel}
Expand Down Expand Up @@ -682,7 +671,7 @@ export default function Chat({ user }: { user: ChatUserShape }) {
{/* Right Sidebar */}
{conversationId && (
<div
className={`w-full sm:w-[320px] flex-shrink-0 border-l border-gray-200 flex flex-col absolute right-0 top-0 bottom-0 h-full z-40 bg-white md:bg-white xl:bg-transparent xl:relative xl:transform-none transition-transform duration-300 ${showRightSidebar ? "translate-x-0" : "translate-x-full xl:translate-x-0 xl:hidden"}`}
className={`w-full sm:w-[320px] flex-shrink-0 border-l border-gray-200 flex flex-col absolute right-0 top-0 bottom-0 h-full z-40 bg-white xl:bg-transparent xl:relative xl:transform-none transition-transform duration-300 ${showRightSidebar ? "translate-x-0" : "translate-x-full xl:translate-x-0 xl:hidden"}`}
>
<div className="absolute top-4 right-4 xl:hidden">
<button
Expand Down Expand Up @@ -839,9 +828,11 @@ export default function Chat({ user }: { user: ChatUserShape }) {
)}

{showModal && (
<div className="fixed inset-0 flex items-center justify-center bg-black/70 z-50 backdrop-blur-sm">
<div className="glass-card w-[400px] p-6">
<h3 className="text-lg font-bold text-gray-900 mb-4">New Message</h3>
<div className="fixed p-5 inset-0 flex items-center justify-center bg-black/70 z-50 backdrop-blur-sm">
<div className="glass-card p-8">
<h3 className="text-lg font-bold text-gray-900 mb-4">
New Message
</h3>
<input
value={search}
onChange={(e) => setSearch(e.target.value)}
Expand Down
22 changes: 17 additions & 5 deletions app/components/auth/VerifyWakatime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,11 @@ export default function VerifyWakatime() {

const verifyWakatimePromise = new Promise<void>(async (resolve, reject) => {
try {
const wakatimeSyncResponse = await fetch(
`/api/wakatime/sync?apiKey=${encodeURIComponent(apiKey)}`,
);
const wakatimeSyncResponse = await fetch(`/api/wakatime/sync`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ api_key: apiKey, save_only: true }),
});
if (!wakatimeSyncResponse.ok)
throw new Error("Failed to sync Wakatime.");

Expand Down Expand Up @@ -86,7 +88,12 @@ export default function VerifyWakatime() {
href="/"
className="flex items-center gap-3 w-fit hover:opacity-80 transition"
>
<Image src="/apple-touch-icon.png" alt="Devpulse Logo" width={40} height={40} />
<Image
src="/apple-touch-icon.png"
alt="Devpulse Logo"
width={40}
height={40}
/>
<span className="text-2xl font-bold tracking-tight text-white">
Devpulse
</span>
Expand Down Expand Up @@ -152,7 +159,12 @@ export default function VerifyWakatime() {
href="/"
className="lg:hidden flex items-center justify-center gap-3 mb-10"
>
<Image src="/apple-touch-icon.png" alt="Devpulse Logo" width={40} height={40} />
<Image
src="/apple-touch-icon.png"
alt="Devpulse Logo"
width={40}
height={40}
/>
<h2 className="text-3xl font-bold text-gray-900">Devpulse</h2>
</Link>

Expand Down
12 changes: 3 additions & 9 deletions app/components/chat/Conversations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,20 +46,14 @@ export default function Conversations({
key={idx}
type="button"
onClick={() => setConversationId(conv.id)}
className={`w-full flex items-center gap-3.5 p-3 rounded-xl transition-all text-left ${
className={`w-full flex items-center bg-gray-100 gap-3.5 p-3 rounded-xl transition-all text-left ${
isActive
? "bg-gray-100 border border-gray-200 shadow-sm"
? "bg-gray-100 border border-gray-300"
: "hover:bg-gray-100 border border-transparent opacity-80 hover:opacity-100"
}`}
>
<div className="relative flex-shrink-0">
<div
className={`flex justify-center items-center w-[38px] h-[38px] rounded-full text-[14px] font-bold transition-all border ${
isGlobal
? "bg-blue-500/15 text-blue-600 border-blue-500/30"
: "bg-neutral-800 text-gray-700 border-gray-200 shadow-sm"
}`}
>
<div className="flex justify-center items-center w-[38px] h-[38px] rounded-full text-[14px] font-bold transition-all border bg-white-800 text-gray-500 border-gray-200">
{initials}
</div>
{!isGlobal && isOnline && (
Expand Down
4 changes: 2 additions & 2 deletions app/components/chat/Messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -225,8 +225,8 @@ export default function Messages({
<div
className={`px-5 py-3 text-[14px] leading-relaxed break-words break-all overflow-x-hidden ${
isSelf
? "bg-blue-600 border border-blue-500/50 text-gray-900 rounded-2xl rounded-br-sm shadow-sm"
: "bg-[rgba(15,15,40,0.6)] border border-blue-200 text-gray-700 rounded-2xl rounded-bl-sm"
? "bg-blue-500 text-white rounded-2xl rounded-br-sm shadow-sm"
: "bg-white border border-gray-200 text-gray-500 rounded-2xl rounded-bl-sm"
}`}
>
<div className="prose prose-invert prose-sm max-w-none break-words break-all whitespace-pre-wrap leading-[1.6]">
Expand Down
4 changes: 2 additions & 2 deletions app/components/chat/hooks/useChatConversationActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ export function useChatConversationActions({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
otheruser_id: otherUser.user_id,
otherUserEmail: otherUser.email,
other_user_id: otherUser.user_id,
other_user_email: otherUser.email,
}),
});

Expand Down
2 changes: 1 addition & 1 deletion app/components/chat/hooks/useChatMessageComposer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function useChatMessageComposer({
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
conversationId: targetConversationId,
conversation_id: targetConversationId,
text: outgoingText,
attachments: [],
}),
Expand Down
2 changes: 1 addition & 1 deletion app/components/chat/hooks/useChatPresence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ export function useChatPresence({
await fetch(`/api/conversations/${targetConversationId}/presence`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ markRead: true }),
body: JSON.stringify({ mark_read: true }),
}).catch(() => {});
},
[
Expand Down
Loading