diff --git a/apps/web/src/app/account/layout.tsx b/apps/web/src/app/account/layout.tsx deleted file mode 100644 index e5fbdf3447..0000000000 --- a/apps/web/src/app/account/layout.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"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"; - -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 && } -
-
- ); -} \ 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 - - -
-
-
- - - - - - - {avatarPreview && ( - - )} -
-
- -

- Upload a profile picture to personalize your account -

-
- - - {avatarFile && ( - - )} -
- {avatarFile && ( -

- Selected: {avatarFile.name} -

- )} -
-
- -
-
- - setName(e.target.value)} - placeholder="Enter your name" - required - /> -
- -
- - setEmail(e.target.value)} - placeholder="Enter your email" - required - /> -
-
- - -
-
-
- - {/* Security Section */} - - - Security - Manage your password and security settings - - -
-
-
- - setCurrentPassword(e.target.value)} - placeholder="Enter current password" - required - /> -
- -
- - setNewPassword(e.target.value)} - placeholder="Enter new password (min 8 characters)" - required - /> -
- -
- - setConfirmPassword(e.target.value)} - placeholder="Confirm new password" - required - /> -
-
- - -
-
-
- - {/* 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/admin/AdminLayoutClient.tsx b/apps/web/src/app/admin/AdminLayoutClient.tsx index 5590191551..f1ebd0cf96 100644 --- a/apps/web/src/app/admin/AdminLayoutClient.tsx +++ b/apps/web/src/app/admin/AdminLayoutClient.tsx @@ -16,8 +16,8 @@ export default function AdminLayoutClient({ pathname.includes('/support') ? 'support' : 'users'; return ( -
-
+
+
Admin Dashboard @@ -28,7 +28,7 @@ export default function AdminLayoutClient({ - + Monitoring diff --git a/apps/web/src/app/admin/users/page.tsx b/apps/web/src/app/admin/users/page.tsx index 6b6f7ad779..c58f5c56fb 100644 --- a/apps/web/src/app/admin/users/page.tsx +++ b/apps/web/src/app/admin/users/page.tsx @@ -82,7 +82,7 @@ export default function AdminUsersPage() { -
+
{[...Array(4)].map((_, i) => (
@@ -145,7 +145,7 @@ export default function AdminUsersPage() { -
+
{users.length}
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, diff --git a/apps/web/src/app/dashboard/storage/page.tsx b/apps/web/src/app/dashboard/storage/page.tsx index 6c6179f4a3..ed4452ac37 100644 --- a/apps/web/src/app/dashboard/storage/page.tsx +++ b/apps/web/src/app/dashboard/storage/page.tsx @@ -142,7 +142,7 @@ export default function StorageDashboard() { if (loading) { return ( -
+
@@ -157,7 +157,7 @@ export default function StorageDashboard() { if (!storageInfo) { return ( -
+
Error @@ -171,7 +171,7 @@ export default function StorageDashboard() { return (
-
+
-
+

Storage Dashboard

@@ -194,6 +194,7 @@ export default function StorageDashboard() { disabled={reconciling} variant="outline" size="sm" + className="w-full sm:w-auto" > Reconcile Storage @@ -233,7 +234,7 @@ export default function StorageDashboard() { value={quota.utilizationPercent} className="h-3" /> -

+
{quota.formattedAvailable} available @@ -247,7 +248,7 @@ export default function StorageDashboard() { {/* Tier Information Card */} - + Storage Plan {tierInfo.name} @@ -257,18 +258,18 @@ export default function StorageDashboard() {
-
+
Total Files: {totalFiles} {tierInfo.maxFileCount > 0 && ` / ${tierInfo.maxFileCount}`}
-
+
Max File Size: {formatBytes(tierInfo.maxFileSize)}
-
+
Concurrent Uploads: {tierInfo.maxConcurrentUploads}
@@ -318,28 +319,30 @@ export default function StorageDashboard() { Storage usage across your drives - - - - Drive Name - Files - Storage Used - Percentage - - - - {storageByDrive.map((drive) => ( - - {drive.driveName} - {drive.fileCount} - {drive.formattedSize} - - {Math.round((drive.totalSize / quota.usedBytes) * 100)}% - +
+
+ + + Drive Name + Files + Storage Used + Percentage - ))} - -
+ + + {storageByDrive.map((drive) => ( + + {drive.driveName} + {drive.fileCount} + {drive.formattedSize} + + {Math.round((drive.totalSize / quota.usedBytes) * 100)}% + + + ))} + + +
)} 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 + + +
+
+
+ + + + + + + {avatarPreview && ( + + )} +
+
+ +

+ Upload a profile picture to personalize your account +

+
+ + + {avatarFile && ( + + )} +
+ {avatarFile && ( +

+ Selected: {avatarFile.name} +

+ )} +
+
+ +
+
+ + setName(e.target.value)} + placeholder="Enter your name" + required + /> +
+ +
+ + setEmail(e.target.value)} + placeholder="Enter your email" + required + /> +
+
+ + +
+
+
+ + {/* Security Section */} + + + Security + Manage your password and security settings + + +
+
+
+ + setCurrentPassword(e.target.value)} + placeholder="Enter current password" + required + /> +
+ +
+ + setNewPassword(e.target.value)} + placeholder="Enter new password (min 8 characters)" + required + /> +
+ +
+ + setConfirmPassword(e.target.value)} + placeholder="Confirm new password" + required + /> +
+
+ + +
+
+
+ + {/* 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/ai/page.tsx b/apps/web/src/app/settings/ai/page.tsx index 3fffcac46d..0ff58a7a2f 100644 --- a/apps/web/src/app/settings/ai/page.tsx +++ b/apps/web/src/app/settings/ai/page.tsx @@ -279,7 +279,7 @@ export default function AiSettingsPage() { -
+
OpenRouter {isProviderConfigured('openrouter') ? ( 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}; +} diff --git a/apps/web/src/app/settings/page.tsx b/apps/web/src/app/settings/page.tsx index 9dc34a32dc..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, }, { @@ -47,7 +47,7 @@ export default function SettingsPage() { ]; return ( -
+
-
+
{settingsCategories.map((category) => ( Content Statistics - - - - Drives - {user.stats.drives} - - - Pages - {user.stats.pages} - - - Page Messages - {user.stats.chatMessages} - - - Drive Messages - {user.stats.driveChatMessages} - - - Global Messages - {user.stats.globalMessages} - - - Active Sessions - {user.stats.refreshTokens} - - -
+
+ + + + Drives + {user.stats.drives} + + + Pages + {user.stats.pages} + + + Page Messages + {user.stats.chatMessages} + + + Drive Messages + {user.stats.driveChatMessages} + + + Global Messages + {user.stats.globalMessages} + + + Active Sessions + {user.stats.refreshTokens} + + +
+
{/* AI Settings and Account Info */} diff --git a/apps/web/src/components/billing/UsageCounter.tsx b/apps/web/src/components/billing/UsageCounter.tsx index a4745cc46d..3bc784c79e 100644 --- a/apps/web/src/components/billing/UsageCounter.tsx +++ b/apps/web/src/components/billing/UsageCounter.tsx @@ -108,7 +108,7 @@ export function UsageCounter() { } return ( -
+
{/* Usage Display */}
diff --git a/apps/web/src/components/layout/Layout.tsx b/apps/web/src/components/layout/Layout.tsx index da4cea22da..2f24adb22a 100644 --- a/apps/web/src/components/layout/Layout.tsx +++ b/apps/web/src/components/layout/Layout.tsx @@ -7,13 +7,15 @@ import CenterPanel from "@/components/layout/middle-content/CenterPanel"; import MemoizedRightPanel from "@/components/layout/right-sidebar/MemoizedRightPanel"; import { NavigationProvider } from "@/components/layout/NavigationProvider"; import { useMobile } from "@/hooks/use-mobile"; +import { useBreakpoint } from "@/hooks/use-breakpoint"; +import { useResponsivePanels } from "@/hooks/use-responsive-panels"; import { motion, AnimatePresence } from "motion/react"; import { DebugPanel } from "./DebugPanel"; import { useLayoutStore } from "@/stores/useLayoutStore"; import { useHasHydrated } from "@/hooks/useHasHydrated"; import { usePerformanceMonitor } from "@/hooks/usePerformanceMonitor"; import { useRouter } from "next/navigation"; -import { useEffect, memo } from "react"; +import { useCallback, useEffect, memo } from "react"; interface LayoutProps { children?: React.ReactNode; @@ -28,8 +30,13 @@ function Layout({ children }: LayoutProps) { rightSidebarOpen, toggleLeftSidebar, toggleRightSidebar, + setLeftSidebarOpen, + setRightSidebarOpen, } = useLayoutStore(); const hasHydrated = useHasHydrated(); + const shouldOverlaySidebars = useBreakpoint("(max-width: 1279px)"); + + useResponsivePanels(); // Monitor performance @@ -61,6 +68,52 @@ function Layout({ children }: LayoutProps) { } }, [hasHydrated, isLoading, isAuthenticated, router]); + const handleLeftPanelToggle = useCallback(() => { + if (shouldOverlaySidebars) { + if (leftSidebarOpen) { + setLeftSidebarOpen(false); + } else { + if (rightSidebarOpen) { + setRightSidebarOpen(false); + } + setLeftSidebarOpen(true); + } + return; + } + + toggleLeftSidebar(); + }, [ + shouldOverlaySidebars, + leftSidebarOpen, + rightSidebarOpen, + setLeftSidebarOpen, + setRightSidebarOpen, + toggleLeftSidebar, + ]); + + const handleRightPanelToggle = useCallback(() => { + if (shouldOverlaySidebars) { + if (rightSidebarOpen) { + setRightSidebarOpen(false); + } else { + if (leftSidebarOpen) { + setLeftSidebarOpen(false); + } + setRightSidebarOpen(true); + } + return; + } + + toggleRightSidebar(); + }, [ + shouldOverlaySidebars, + leftSidebarOpen, + rightSidebarOpen, + setLeftSidebarOpen, + setRightSidebarOpen, + toggleRightSidebar, + ]); + // Optimize loading checks - show UI earlier for better perceived performance if (isLoading || !hasHydrated) { return ( @@ -87,91 +140,93 @@ function Layout({ children }: LayoutProps) { return ( -
+
- -
- {/* Desktop Left Sidebar */} - {leftSidebarOpen && !isMobile && ( -
- + +
+ {!shouldOverlaySidebars && leftSidebarOpen && ( +
+
)} - {/* Mobile Left Sidebar */} - {isMobile && leftSidebarOpen && ( - <> - - - - - {/* Mobile Overlay */} - - + {shouldOverlaySidebars && leftSidebarOpen && ( + +
+ +
+
)}
- {/* Main Content Area */} -
- {/* Use our center panel instead of children */} - {children || } +
+ {children ? ( +
+ {children} +
+ ) : ( + + )}
- {/* Desktop Right Sidebar */} - {rightSidebarOpen && !isMobile && ( -
- + {!shouldOverlaySidebars && rightSidebarOpen && ( +
+
)} - {/* Mobile Right Sidebar */} - {isMobile && rightSidebarOpen && ( - <> - - - - - {/* Mobile Overlay (if not already shown for left sidebar) */} - {!leftSidebarOpen && ( - - )} - + {shouldOverlaySidebars && rightSidebarOpen && ( + +
+ +
+
+ )} +
+ + + {shouldOverlaySidebars && (leftSidebarOpen || rightSidebarOpen) && ( + { + if (leftSidebarOpen) { + setLeftSidebarOpen(false); + } + if (rightSidebarOpen) { + setRightSidebarOpen(false); + } + }} + /> )}
- - {/* Debug Panel for Development */} +
diff --git a/apps/web/src/components/layout/left-sidebar/MemoizedSidebar.tsx b/apps/web/src/components/layout/left-sidebar/MemoizedSidebar.tsx index 4c412b72ea..18f23b6c13 100644 --- a/apps/web/src/components/layout/left-sidebar/MemoizedSidebar.tsx +++ b/apps/web/src/components/layout/left-sidebar/MemoizedSidebar.tsx @@ -2,7 +2,7 @@ import { memo } from 'react'; import { usePathname } from 'next/navigation'; -import Sidebar from './index'; +import Sidebar, { type SidebarProps } from './index'; import MessagesLeftSidebar from './MessagesLeftSidebar'; /** @@ -10,15 +10,15 @@ import MessagesLeftSidebar from './MessagesLeftSidebar'; * This helps avoid infinite loops in the layout system * Renders MessagesLeftSidebar when on messages route */ -const MemoizedSidebar = memo(() => { +const MemoizedSidebar = memo((props: SidebarProps) => { const pathname = usePathname(); const isMessagesRoute = pathname?.startsWith('/dashboard/messages'); if (isMessagesRoute) { - return ; + return ; } - return ; + return ; }); MemoizedSidebar.displayName = 'MemoizedSidebar'; diff --git a/apps/web/src/components/layout/left-sidebar/MessagesLeftSidebar.tsx b/apps/web/src/components/layout/left-sidebar/MessagesLeftSidebar.tsx index 7029562a95..7f2053a292 100644 --- a/apps/web/src/components/layout/left-sidebar/MessagesLeftSidebar.tsx +++ b/apps/web/src/components/layout/left-sidebar/MessagesLeftSidebar.tsx @@ -11,6 +11,7 @@ import { formatDistanceToNow } from 'date-fns'; import useSWR from 'swr'; import Link from 'next/link'; import { cn } from '@/lib/utils'; +import type { SidebarProps } from './index'; const fetcher = (url: string) => fetch(url).then((res) => res.json()); @@ -38,7 +39,7 @@ interface ConversationsResponse { }; } -export default function MessagesLeftSidebar() { +export default function MessagesLeftSidebar({ className, variant = 'desktop' }: SidebarProps) { const router = useRouter(); const params = useParams(); const [searchQuery, setSearchQuery] = useState(''); @@ -89,8 +90,14 @@ export default function MessagesLeftSidebar() { }; return ( -