From 1c4845bae9275d51aee1c0f5fe06a7287a045de2 Mon Sep 17 00:00:00 2001
From: 2Wits <2witstudios@gmail.com>
Date: Wed, 24 Sep 2025 22:37:25 -0500
Subject: [PATCH 4/8] Use shared layout for settings pages
---
apps/web/src/app/settings/layout.tsx | 52 ++--------------------------
1 file changed, 3 insertions(+), 49 deletions(-)
diff --git a/apps/web/src/app/settings/layout.tsx b/apps/web/src/app/settings/layout.tsx
index b6b33b280e..1dae82f3cc 100644
--- a/apps/web/src/app/settings/layout.tsx
+++ b/apps/web/src/app/settings/layout.tsx
@@ -1,53 +1,7 @@
"use client";
-import { useAuth } from "@/hooks/use-auth";
-import { useRouter } from "next/navigation";
-import { useEffect } from "react";
-import TopBar from "@/components/layout/main-header";
-import Sidebar from "@/components/layout/left-sidebar";
-import RightPanel from "@/components/layout/right-sidebar";
-import { useLayoutStore } from "@/stores/useLayoutStore";
-import { useHasHydrated } from "@/hooks/useHasHydrated";
+import Layout from "@/components/layout/Layout";
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
- const { isLoading, isAuthenticated } = useAuth();
- const router = useRouter();
- const {
- leftSidebarOpen,
- rightSidebarOpen,
- toggleLeftSidebar,
- toggleRightSidebar,
- } = useLayoutStore();
- const hasHydrated = useHasHydrated();
-
- useEffect(() => {
- if (!isLoading && !isAuthenticated) {
- router.push('/auth/signin');
- }
- }, [isLoading, isAuthenticated, router]);
-
- if (isLoading || !hasHydrated) {
- return
Loading...
;
- }
-
- if (!isAuthenticated) {
- router.push("/auth/signin");
- return null;
- }
-
- return (
-
-
-
- {leftSidebarOpen && }
-
- {children}
-
- {rightSidebarOpen && }
-
-
- );
-}
\ No newline at end of file
+ return
{children};
+}
From 4cbdc20e1057b4548c4fd90b60531a3d96f6edea Mon Sep 17 00:00:00 2001
From: 2Wits <2witstudios@gmail.com>
Date: Wed, 24 Sep 2025 22:57:25 -0500
Subject: [PATCH 5/8] Make settings content scrollable
---
apps/web/src/components/layout/Layout.tsx | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/apps/web/src/components/layout/Layout.tsx b/apps/web/src/components/layout/Layout.tsx
index 3e691050b5..2f24adb22a 100644
--- a/apps/web/src/components/layout/Layout.tsx
+++ b/apps/web/src/components/layout/Layout.tsx
@@ -171,7 +171,13 @@ function Layout({ children }: LayoutProps) {
- {children || }
+ {children ? (
+
+ {children}
+
+ ) : (
+
+ )}
{!shouldOverlaySidebars && rightSidebarOpen && (
From f4f5751770810276ebb2fed91505c71dec29e725 Mon Sep 17 00:00:00 2001
From: 2Wits <2witstudios@gmail.com>
Date: Wed, 24 Sep 2025 23:13:55 -0500
Subject: [PATCH 6/8] Align account layout with shared shell
---
apps/web/src/app/account/layout.tsx | 50 ++---------------------------
1 file changed, 2 insertions(+), 48 deletions(-)
diff --git a/apps/web/src/app/account/layout.tsx b/apps/web/src/app/account/layout.tsx
index e5fbdf3447..ee7d5236aa 100644
--- a/apps/web/src/app/account/layout.tsx
+++ b/apps/web/src/app/account/layout.tsx
@@ -1,53 +1,7 @@
"use client";
-import { useAuth } from "@/hooks/use-auth";
-import { useRouter } from "next/navigation";
-import { useEffect } from "react";
-import TopBar from "@/components/layout/main-header";
-import Sidebar from "@/components/layout/left-sidebar";
-import RightPanel from "@/components/layout/right-sidebar";
-import { useLayoutStore } from "@/stores/useLayoutStore";
-import { useHasHydrated } from "@/hooks/useHasHydrated";
+import Layout from "@/components/layout/Layout";
export default function AccountLayout({ children }: { children: React.ReactNode }) {
- const { isLoading, isAuthenticated } = useAuth();
- const router = useRouter();
- const {
- leftSidebarOpen,
- rightSidebarOpen,
- toggleLeftSidebar,
- toggleRightSidebar,
- } = useLayoutStore();
- const hasHydrated = useHasHydrated();
-
- useEffect(() => {
- if (!isLoading && !isAuthenticated) {
- router.push('/auth/signin');
- }
- }, [isLoading, isAuthenticated, router]);
-
- if (isLoading || !hasHydrated) {
- return
Loading...
;
- }
-
- if (!isAuthenticated) {
- router.push("/auth/signin");
- return null;
- }
-
- return (
-
-
-
- {leftSidebarOpen && }
-
- {children}
-
- {rightSidebarOpen && }
-
-
- );
+ return
{children};
}
\ No newline at end of file
From 1220f5aa12ed7f064dec403755cdddbcc1f86342 Mon Sep 17 00:00:00 2001
From: 2Wits <2witstudios@gmail.com>
Date: Wed, 24 Sep 2025 23:14:01 -0500
Subject: [PATCH 7/8] Move account page under settings
---
apps/web/src/app/account/layout.tsx | 7 -
apps/web/src/app/account/page.tsx | 464 +-----------------
apps/web/src/app/settings/account/page.tsx | 461 +++++++++++++++++
apps/web/src/app/settings/page.tsx | 2 +-
.../src/components/shared/UserDropdown.tsx | 2 +-
apps/web/src/lib/canvas/sample-dashboard.html | 4 +-
.../building-canvas-dashboards.md | 2 +-
7 files changed, 470 insertions(+), 472 deletions(-)
delete mode 100644 apps/web/src/app/account/layout.tsx
create mode 100644 apps/web/src/app/settings/account/page.tsx
diff --git a/apps/web/src/app/account/layout.tsx b/apps/web/src/app/account/layout.tsx
deleted file mode 100644
index ee7d5236aa..0000000000
--- a/apps/web/src/app/account/layout.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-"use client";
-
-import Layout from "@/components/layout/Layout";
-
-export default function AccountLayout({ children }: { children: React.ReactNode }) {
- return
{children};
-}
\ No newline at end of file
diff --git a/apps/web/src/app/account/page.tsx b/apps/web/src/app/account/page.tsx
index fc3e9b2b40..5926f55be4 100644
--- a/apps/web/src/app/account/page.tsx
+++ b/apps/web/src/app/account/page.tsx
@@ -1,461 +1,5 @@
-"use client";
+import { redirect } from "next/navigation";
-import { useState, useEffect } from "react";
-import { useAuth } from "@/hooks/use-auth";
-import { useRouter } from "next/navigation";
-import { Button } from "@/components/ui/button";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
-import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
-import { Alert, AlertDescription } from "@/components/ui/alert";
-import { toast } from "sonner";
-import { User, Mail, Calendar, AlertTriangle, Loader2, ArrowLeft, Upload, X } from "lucide-react";
-
-export default function AccountPage() {
- const { user, isLoading: authLoading, mutate } = useAuth();
- const router = useRouter();
-
- // Profile form state
- const [name, setName] = useState("");
- const [email, setEmail] = useState("");
- const [isSavingProfile, setIsSavingProfile] = useState(false);
-
- // Avatar state
- const [avatarFile, setAvatarFile] = useState
(null);
- const [avatarPreview, setAvatarPreview] = useState(null);
- const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
-
- // Password form state
- const [currentPassword, setCurrentPassword] = useState("");
- const [newPassword, setNewPassword] = useState("");
- const [confirmPassword, setConfirmPassword] = useState("");
- const [isSavingPassword, setIsSavingPassword] = useState(false);
-
- // Load user data into form
- useEffect(() => {
- if (user) {
- setName(user.name || "");
- setEmail(user.email || "");
- if (user.image) {
- setAvatarPreview(user.image);
- }
- }
- }, [user]);
-
- // Redirect if not authenticated
- useEffect(() => {
- if (!authLoading && !user) {
- router.push("/auth/signin");
- }
- }, [authLoading, user, router]);
-
- const handleProfileUpdate = async (e: React.FormEvent) => {
- e.preventDefault();
- setIsSavingProfile(true);
-
- try {
- const response = await fetch("/api/account", {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({ name, email }),
- });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.error || "Failed to update profile");
- }
-
- if (mutate) {
- await mutate(); // Refresh user data
- }
- toast.success("Profile updated successfully");
- } catch (error) {
- console.error("Profile update error:", error);
- toast.error(error instanceof Error ? error.message : "Failed to update profile");
- } finally {
- setIsSavingProfile(false);
- }
- };
-
- const handleAvatarSelect = (e: React.ChangeEvent) => {
- const file = e.target.files?.[0];
- if (!file) return;
-
- // Validate file type
- if (!file.type.startsWith('image/')) {
- toast.error('Please select an image file');
- return;
- }
-
- // Validate file size (5MB)
- if (file.size > 5 * 1024 * 1024) {
- toast.error('Image must be less than 5MB');
- return;
- }
-
- setAvatarFile(file);
-
- // Create preview
- const reader = new FileReader();
- reader.onloadend = () => {
- setAvatarPreview(reader.result as string);
- };
- reader.readAsDataURL(file);
- };
-
- const handleAvatarUpload = async () => {
- if (!avatarFile) return;
-
- setIsUploadingAvatar(true);
- const formData = new FormData();
- formData.append('file', avatarFile);
-
- try {
- const response = await fetch('/api/account/avatar', {
- method: 'POST',
- body: formData,
- });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.error || 'Failed to upload avatar');
- }
-
- await response.json();
- toast.success('Avatar uploaded successfully');
- setAvatarFile(null);
-
- // Refresh user data to get new avatar URL
- if (mutate) {
- await mutate();
- }
- } catch (error) {
- console.error('Avatar upload error:', error);
- toast.error(error instanceof Error ? error.message : 'Failed to upload avatar');
- } finally {
- setIsUploadingAvatar(false);
- }
- };
-
- const handleAvatarDelete = async () => {
- setIsUploadingAvatar(true);
-
- try {
- const response = await fetch('/api/account/avatar', {
- method: 'DELETE',
- });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.error || 'Failed to delete avatar');
- }
-
- toast.success('Avatar deleted successfully');
- setAvatarPreview(null);
- setAvatarFile(null);
-
- // Refresh user data
- if (mutate) {
- await mutate();
- }
- } catch (error) {
- console.error('Avatar deletion error:', error);
- toast.error(error instanceof Error ? error.message : 'Failed to delete avatar');
- } finally {
- setIsUploadingAvatar(false);
- }
- };
-
- const handlePasswordChange = async (e: React.FormEvent) => {
- e.preventDefault();
-
- if (newPassword !== confirmPassword) {
- toast.error("New passwords don't match");
- return;
- }
-
- if (newPassword.length < 8) {
- toast.error("Password must be at least 8 characters long");
- return;
- }
-
- setIsSavingPassword(true);
-
- try {
- const response = await fetch("/api/account/password", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- currentPassword,
- newPassword,
- }),
- });
-
- if (!response.ok) {
- const error = await response.json();
- throw new Error(error.error || "Failed to change password");
- }
-
- toast.success("Password changed successfully");
- // Clear password fields
- setCurrentPassword("");
- setNewPassword("");
- setConfirmPassword("");
- } catch (error) {
- console.error("Password change error:", error);
- toast.error(error instanceof Error ? error.message : "Failed to change password");
- } finally {
- setIsSavingPassword(false);
- }
- };
-
- if (authLoading || !user) {
- return (
-
-
-
- );
- }
-
- const memberSince = user.id ? new Date().toLocaleDateString("en-US", {
- month: "long",
- year: "numeric",
- }) : "Unknown";
-
- return (
-
-
-
-
Account Settings
-
- Manage your account information and security settings
-
-
-
- {/* Profile Section */}
-
-
- Profile Information
- Update your personal information
-
-
-
-
-
-
- {/* Security Section */}
-
-
- Security
- Manage your password and security settings
-
-
-
-
-
-
- {/* Account Info */}
-
-
- Account Information
- View your account details
-
-
-
-
- Member since:
- {memberSince}
-
-
-
- Account ID:
- {user.id}
-
-
-
-
- {/* Danger Zone */}
-
-
- Danger Zone
- Irreversible actions for your account
-
-
-
-
-
- Account deletion is permanent and cannot be undone. This feature is coming soon.
-
-
-
-
-
-
- );
-}
\ No newline at end of file
+export default function AccountRedirectPage() {
+ redirect("/settings/account");
+}
diff --git a/apps/web/src/app/settings/account/page.tsx b/apps/web/src/app/settings/account/page.tsx
new file mode 100644
index 0000000000..fc3e9b2b40
--- /dev/null
+++ b/apps/web/src/app/settings/account/page.tsx
@@ -0,0 +1,461 @@
+"use client";
+
+import { useState, useEffect } from "react";
+import { useAuth } from "@/hooks/use-auth";
+import { useRouter } from "next/navigation";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { toast } from "sonner";
+import { User, Mail, Calendar, AlertTriangle, Loader2, ArrowLeft, Upload, X } from "lucide-react";
+
+export default function AccountPage() {
+ const { user, isLoading: authLoading, mutate } = useAuth();
+ const router = useRouter();
+
+ // Profile form state
+ const [name, setName] = useState("");
+ const [email, setEmail] = useState("");
+ const [isSavingProfile, setIsSavingProfile] = useState(false);
+
+ // Avatar state
+ const [avatarFile, setAvatarFile] = useState(null);
+ const [avatarPreview, setAvatarPreview] = useState(null);
+ const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
+
+ // Password form state
+ const [currentPassword, setCurrentPassword] = useState("");
+ const [newPassword, setNewPassword] = useState("");
+ const [confirmPassword, setConfirmPassword] = useState("");
+ const [isSavingPassword, setIsSavingPassword] = useState(false);
+
+ // Load user data into form
+ useEffect(() => {
+ if (user) {
+ setName(user.name || "");
+ setEmail(user.email || "");
+ if (user.image) {
+ setAvatarPreview(user.image);
+ }
+ }
+ }, [user]);
+
+ // Redirect if not authenticated
+ useEffect(() => {
+ if (!authLoading && !user) {
+ router.push("/auth/signin");
+ }
+ }, [authLoading, user, router]);
+
+ const handleProfileUpdate = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsSavingProfile(true);
+
+ try {
+ const response = await fetch("/api/account", {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ name, email }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || "Failed to update profile");
+ }
+
+ if (mutate) {
+ await mutate(); // Refresh user data
+ }
+ toast.success("Profile updated successfully");
+ } catch (error) {
+ console.error("Profile update error:", error);
+ toast.error(error instanceof Error ? error.message : "Failed to update profile");
+ } finally {
+ setIsSavingProfile(false);
+ }
+ };
+
+ const handleAvatarSelect = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0];
+ if (!file) return;
+
+ // Validate file type
+ if (!file.type.startsWith('image/')) {
+ toast.error('Please select an image file');
+ return;
+ }
+
+ // Validate file size (5MB)
+ if (file.size > 5 * 1024 * 1024) {
+ toast.error('Image must be less than 5MB');
+ return;
+ }
+
+ setAvatarFile(file);
+
+ // Create preview
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ setAvatarPreview(reader.result as string);
+ };
+ reader.readAsDataURL(file);
+ };
+
+ const handleAvatarUpload = async () => {
+ if (!avatarFile) return;
+
+ setIsUploadingAvatar(true);
+ const formData = new FormData();
+ formData.append('file', avatarFile);
+
+ try {
+ const response = await fetch('/api/account/avatar', {
+ method: 'POST',
+ body: formData,
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || 'Failed to upload avatar');
+ }
+
+ await response.json();
+ toast.success('Avatar uploaded successfully');
+ setAvatarFile(null);
+
+ // Refresh user data to get new avatar URL
+ if (mutate) {
+ await mutate();
+ }
+ } catch (error) {
+ console.error('Avatar upload error:', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to upload avatar');
+ } finally {
+ setIsUploadingAvatar(false);
+ }
+ };
+
+ const handleAvatarDelete = async () => {
+ setIsUploadingAvatar(true);
+
+ try {
+ const response = await fetch('/api/account/avatar', {
+ method: 'DELETE',
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || 'Failed to delete avatar');
+ }
+
+ toast.success('Avatar deleted successfully');
+ setAvatarPreview(null);
+ setAvatarFile(null);
+
+ // Refresh user data
+ if (mutate) {
+ await mutate();
+ }
+ } catch (error) {
+ console.error('Avatar deletion error:', error);
+ toast.error(error instanceof Error ? error.message : 'Failed to delete avatar');
+ } finally {
+ setIsUploadingAvatar(false);
+ }
+ };
+
+ const handlePasswordChange = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (newPassword !== confirmPassword) {
+ toast.error("New passwords don't match");
+ return;
+ }
+
+ if (newPassword.length < 8) {
+ toast.error("Password must be at least 8 characters long");
+ return;
+ }
+
+ setIsSavingPassword(true);
+
+ try {
+ const response = await fetch("/api/account/password", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ currentPassword,
+ newPassword,
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json();
+ throw new Error(error.error || "Failed to change password");
+ }
+
+ toast.success("Password changed successfully");
+ // Clear password fields
+ setCurrentPassword("");
+ setNewPassword("");
+ setConfirmPassword("");
+ } catch (error) {
+ console.error("Password change error:", error);
+ toast.error(error instanceof Error ? error.message : "Failed to change password");
+ } finally {
+ setIsSavingPassword(false);
+ }
+ };
+
+ if (authLoading || !user) {
+ return (
+
+
+
+ );
+ }
+
+ const memberSince = user.id ? new Date().toLocaleDateString("en-US", {
+ month: "long",
+ year: "numeric",
+ }) : "Unknown";
+
+ return (
+
+
+
+
Account Settings
+
+ Manage your account information and security settings
+
+
+
+ {/* Profile Section */}
+
+
+ Profile Information
+ Update your personal information
+
+
+
+
+
+
+ {/* Security Section */}
+
+
+ Security
+ Manage your password and security settings
+
+
+
+
+
+
+ {/* Account Info */}
+
+
+ Account Information
+ View your account details
+
+
+
+
+ Member since:
+ {memberSince}
+
+
+
+ Account ID:
+ {user.id}
+
+
+
+
+ {/* Danger Zone */}
+
+
+ Danger Zone
+ Irreversible actions for your account
+
+
+
+
+
+ Account deletion is permanent and cannot be undone. This feature is coming soon.
+
+
+
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx
index 77b79ad482..d4f3a3e461 100644
--- a/apps/web/src/app/settings/page.tsx
+++ b/apps/web/src/app/settings/page.tsx
@@ -20,7 +20,7 @@ export default function SettingsPage() {
title: "Account",
description: "Manage your account and profile",
icon: User,
- href: "/account",
+ href: "/settings/account",
available: true,
},
{
diff --git a/apps/web/src/components/shared/UserDropdown.tsx b/apps/web/src/components/shared/UserDropdown.tsx
index 36a5f64c6b..7df799c0e0 100644
--- a/apps/web/src/components/shared/UserDropdown.tsx
+++ b/apps/web/src/components/shared/UserDropdown.tsx
@@ -108,7 +108,7 @@ export default function UserDropdown() {
Billing ({subscriptionInfo?.subscriptionTier === 'free' ? 'Free' : subscriptionInfo?.subscriptionTier === 'pro' ? 'Pro' : 'Business'})
- router.push('/account')}>
+ router.push('/settings/account')}>
Account
diff --git a/apps/web/src/lib/canvas/sample-dashboard.html b/apps/web/src/lib/canvas/sample-dashboard.html
index 40158662ee..75e61eb50a 100644
--- a/apps/web/src/lib/canvas/sample-dashboard.html
+++ b/apps/web/src/lib/canvas/sample-dashboard.html
@@ -219,7 +219,7 @@
💬 Messages
🔗 Connections
⚙️ Settings
- 👤 Account
+ 👤 Account
@@ -269,7 +269,7 @@ GitHub
diff --git a/docs/3.0-guides-and-tools/building-canvas-dashboards.md b/docs/3.0-guides-and-tools/building-canvas-dashboards.md
index 6116cc204d..ce636f200b 100644
--- a/docs/3.0-guides-and-tools/building-canvas-dashboards.md
+++ b/docs/3.0-guides-and-tools/building-canvas-dashboards.md
@@ -180,7 +180,7 @@ Canvas dashboards support multiple ways to create navigation:
Home
Specific Page
Settings
-
Account
+
Account
```
### Getting Your Drive and Page IDs
From 7f3e50c3d43d18bfe0f0ff69e836e2ebf5c66ae8 Mon Sep 17 00:00:00 2001
From: DaisyDebate <2witstudios@gmail.com>
Date: Wed, 24 Sep 2025 23:32:45 -0500
Subject: [PATCH 8/8] fixed DM route
---
.../app/api/messages/conversations/route.ts | 92 +++++++++----------
1 file changed, 46 insertions(+), 46 deletions(-)
diff --git a/apps/web/src/app/api/messages/conversations/route.ts b/apps/web/src/app/api/messages/conversations/route.ts
index fcd99a57f6..4c65a1c49f 100644
--- a/apps/web/src/app/api/messages/conversations/route.ts
+++ b/apps/web/src/app/api/messages/conversations/route.ts
@@ -21,75 +21,75 @@ export async function GET(request: Request) {
WITH conversation_data AS (
SELECT
c.id,
- c.participant1_id,
- c.participant2_id,
- c.last_message_at,
- c.last_message_preview,
- c.participant1_last_read,
- c.participant2_last_read,
- c.created_at,
+ c."participant1Id",
+ c."participant2Id",
+ c."lastMessageAt",
+ c."lastMessagePreview",
+ c."participant1LastRead",
+ c."participant2LastRead",
+ c."createdAt",
CASE
- WHEN c.participant1_id = ${user.id} THEN c.participant2_id
- ELSE c.participant1_id
+ WHEN c."participant1Id" = ${user.id} THEN c."participant2Id"
+ ELSE c."participant1Id"
END as other_user_id,
CASE
- WHEN c.participant1_id = ${user.id} THEN c.participant1_last_read
- ELSE c.participant2_last_read
+ WHEN c."participant1Id" = ${user.id} THEN c."participant1LastRead"
+ ELSE c."participant2LastRead"
END as last_read
FROM dm_conversations c
- WHERE c.participant1_id = ${user.id} OR c.participant2_id = ${user.id}
+ WHERE c."participant1Id" = ${user.id} OR c."participant2Id" = ${user.id}
${cursor ? (direction === 'before'
- ? sql`AND c.last_message_at > ${cursor}`
- : sql`AND c.last_message_at < ${cursor}`)
+ ? sql`AND c."lastMessageAt" > ${cursor}`
+ : sql`AND c."lastMessageAt" < ${cursor}`)
: sql``}
- ORDER BY c.last_message_at DESC NULLS LAST
+ ORDER BY c."lastMessageAt" DESC NULLS LAST
LIMIT ${limit}
),
unread_counts AS (
SELECT
- dm.conversation_id,
+ dm."conversationId",
COUNT(*) as unread_count
FROM direct_messages dm
- INNER JOIN conversation_data cd ON dm.conversation_id = cd.id
- WHERE dm.sender_id = cd.other_user_id
- AND dm.is_read = false
- GROUP BY dm.conversation_id
+ INNER JOIN conversation_data cd ON dm."conversationId" = cd.id
+ WHERE dm."senderId" = cd.other_user_id
+ AND dm."isRead" = false
+ GROUP BY dm."conversationId"
)
SELECT
cd.id,
- cd.participant1_id,
- cd.participant2_id,
- cd.last_message_at,
- cd.last_message_preview,
- cd.participant1_last_read,
- cd.participant2_last_read,
- cd.created_at,
+ cd."participant1Id",
+ cd."participant2Id",
+ cd."lastMessageAt",
+ cd."lastMessagePreview",
+ cd."participant1LastRead",
+ cd."participant2LastRead",
+ cd."createdAt",
cd.last_read,
u.id as other_user_id,
u.name as other_user_name,
u.email as other_user_email,
u.image as other_user_image,
up.username as other_user_username,
- up.display_name as other_user_display_name,
- up.avatar_url as other_user_avatar_url,
+ up."displayName" as other_user_display_name,
+ up."avatarUrl" as other_user_avatar_url,
COALESCE(uc.unread_count, 0) as unread_count
FROM conversation_data cd
LEFT JOIN users u ON u.id = cd.other_user_id
- LEFT JOIN user_profiles up ON up.user_id = cd.other_user_id
- LEFT JOIN unread_counts uc ON uc.conversation_id = cd.id
- ORDER BY cd.last_message_at DESC NULLS LAST
+ LEFT JOIN user_profiles up ON up."userId" = cd.other_user_id
+ LEFT JOIN unread_counts uc ON uc."conversationId" = cd.id
+ ORDER BY cd."lastMessageAt" DESC NULLS LAST
`);
// Transform the raw results to match the expected format
interface ConversationRow {
id: string;
- participant1_id: string;
- participant2_id: string;
- last_message_at: string | null;
- last_message_preview: string | null;
- participant1_last_read: string | null;
- participant2_last_read: string | null;
- created_at: string;
+ participant1Id: string;
+ participant2Id: string;
+ lastMessageAt: string | null;
+ lastMessagePreview: string | null;
+ participant1LastRead: string | null;
+ participant2LastRead: string | null;
+ createdAt: string;
last_read: string | null;
other_user_id: string;
other_user_name: string;
@@ -105,13 +105,13 @@ export async function GET(request: Request) {
const typedRow = row as unknown as ConversationRow;
return {
id: typedRow.id,
- participant1Id: typedRow.participant1_id,
- participant2Id: typedRow.participant2_id,
- lastMessageAt: typedRow.last_message_at,
- lastMessagePreview: typedRow.last_message_preview,
- participant1LastRead: typedRow.participant1_last_read,
- participant2LastRead: typedRow.participant2_last_read,
- createdAt: typedRow.created_at,
+ participant1Id: typedRow.participant1Id,
+ participant2Id: typedRow.participant2Id,
+ lastMessageAt: typedRow.lastMessageAt,
+ lastMessagePreview: typedRow.lastMessagePreview,
+ participant1LastRead: typedRow.participant1LastRead,
+ participant2LastRead: typedRow.participant2LastRead,
+ createdAt: typedRow.createdAt,
lastRead: typedRow.last_read,
otherUser: {
id: typedRow.other_user_id,