-
Notifications
You must be signed in to change notification settings - Fork 0
Feat/dashboard overhaul #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 9 commits
dc6be89
1392f36
bfb4f5f
3e1ea30
60a74ba
e59e2cb
7ffb99d
ca5e468
25d9ce3
08b1850
4eda08a
33cb45f
57722ac
796a83f
f8c08d8
2b43d29
629ed27
60c0a37
66fe065
9acea26
38a4f0a
c0e103c
3ea2f78
9b03ffd
ded9586
197bc76
5b1bd29
c09e0c5
33ac80b
c5c702e
5eb0200
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useCallback, useMemo } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { toast } from 'sonner'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useAuth } from '@/lib/auth'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useWebSocket } from '@/lib/hooks'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Sidebar } from '@/components/dashboard'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function DashboardShell({ children }: { children: React.ReactNode }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { isAuthenticated } = useAuth(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handlePoke = useCallback((payload: unknown) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const p = payload as { fromUserId: string; message?: string }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toast.info(p.message || 'Someone poked you!'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleAchievement = useCallback((payload: unknown) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const p = payload as { achievement: { title: string; description: string } }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toast.success(`Achievement unlocked: ${p.achievement.title}`); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const p = payload as { fromUserId: string; message?: string }; | |
| toast.info(p.message || 'Someone poked you!'); | |
| }, []); | |
| const handleAchievement = useCallback((payload: unknown) => { | |
| const p = payload as { achievement: { title: string; description: string } }; | |
| toast.success(`Achievement unlocked: ${p.achievement.title}`); | |
| if (!payload || typeof payload !== 'object') { | |
| // Fallback to default message if payload is malformed | |
| toast.info('Someone poked you!'); | |
| return; | |
| } | |
| const possible = payload as { fromUserId?: unknown; message?: unknown }; | |
| const message = | |
| typeof possible.message === 'string' && possible.message.trim().length > 0 | |
| ? possible.message | |
| : 'Someone poked you!'; | |
| toast.info(message); | |
| }, []); | |
| const handleAchievement = useCallback((payload: unknown) => { | |
| if (!payload || typeof payload !== 'object' || !('achievement' in payload)) { | |
| // Ignore malformed achievement payloads | |
| return; | |
| } | |
| const achievement = (payload as { achievement?: unknown }).achievement; | |
| if (!achievement || typeof achievement !== 'object') { | |
| return; | |
| } | |
| const maybeAchievement = achievement as { title?: unknown; description?: unknown }; | |
| if (typeof maybeAchievement.title !== 'string') { | |
| return; | |
| } | |
| toast.success(`Achievement unlocked: ${maybeAchievement.title}`); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,227 @@ | ||||||||||||||||||||||||||||||||||||||||||||||
| 'use client'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import { useState, useEffect, useCallback } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { toast } from 'sonner'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| import { useAuth } from '@/lib/auth'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { friendsApi, friendRequestsApi } from '@/lib/api'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import type { Friend, Follower, FriendRequest } from '@/lib/api'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { FriendItem, RequestCard } from '@/components/dashboard/friend-card'; | ||||||||||||||||||||||||||||||||||||||||||||||
| import { UserSearch } from '@/components/dashboard/user-search'; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| export default function FriendsPage() { | ||||||||||||||||||||||||||||||||||||||||||||||
| const { isAuthenticated } = useAuth(); | ||||||||||||||||||||||||||||||||||||||||||||||
| const [friends, setFriends] = useState<Friend[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||
| const [followers, setFollowers] = useState<Follower[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||
| const [incoming, setIncoming] = useState<FriendRequest[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||
| const [outgoing, setOutgoing] = useState<FriendRequest[]>([]); | ||||||||||||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = useState(true); | ||||||||||||||||||||||||||||||||||||||||||||||
| const [actionLoading, setActionLoading] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const fetchAll = useCallback(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(true); | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| const [fr, fo, inc, out] = await Promise.allSettled([ | ||||||||||||||||||||||||||||||||||||||||||||||
| friendsApi.list(1, 50), | ||||||||||||||||||||||||||||||||||||||||||||||
| friendsApi.followers(1, 50), | ||||||||||||||||||||||||||||||||||||||||||||||
| friendRequestsApi.incoming(1, 50), | ||||||||||||||||||||||||||||||||||||||||||||||
| friendRequestsApi.outgoing(1, 50), | ||||||||||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if (fr.status === 'fulfilled') setFriends(fr.value.data); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (fo.status === 'fulfilled') setFollowers(fo.value.data); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (inc.status === 'fulfilled') setIncoming(inc.value.data); | ||||||||||||||||||||||||||||||||||||||||||||||
| if (out.status === 'fulfilled') setOutgoing(out.value.data); | ||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||
| if (isAuthenticated) fetchAll(); | ||||||||||||||||||||||||||||||||||||||||||||||
| }, [isAuthenticated, fetchAll]); | ||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const handleUnfollow = async (id: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| setActionLoading(id); | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| await friendsApi.unfollow(id); | ||||||||||||||||||||||||||||||||||||||||||||||
| setFriends((prev) => prev.filter((f) => f.id !== id)); | ||||||||||||||||||||||||||||||||||||||||||||||
| toast.success('Unfollowed'); | ||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||
| toast.error('Failed to unfollow'); | ||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||
| setActionLoading(null); | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| const handleAccept = async (id: string) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| setActionLoading(id); | ||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||
| await friendRequestsApi.accept(id); | ||||||||||||||||||||||||||||||||||||||||||||||
| setIncoming((prev) => prev.filter((r) => r.id !== id)); | ||||||||||||||||||||||||||||||||||||||||||||||
| toast.success('Request accepted'); | ||||||||||||||||||||||||||||||||||||||||||||||
| fetchAll(); | ||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||
| fetchAll(); | |
| try { | |
| const friendsRes = await friendsApi.list(1, 50); | |
| setFriends(friendsRes.data); | |
| } catch { | |
| // Ignore errors when refreshing friends list; request was accepted successfully. | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleCancel is missing a success toast, unlike all other action handlers.
handleUnfollow, handleAccept, and handleReject all call toast.success(...) on completion, but handleCancel silently removes the item.
♻️ Proposed fix
try {
await friendRequestsApi.cancel(id);
setOutgoing((prev) => prev.filter((r) => r.id !== id));
+ toast.success('Request cancelled');
} catch {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleCancel = async (id: string) => { | |
| setActionLoading(id); | |
| try { | |
| await friendRequestsApi.cancel(id); | |
| setOutgoing((prev) => prev.filter((r) => r.id !== id)); | |
| } catch { | |
| toast.error('Failed to cancel'); | |
| } finally { | |
| setActionLoading(null); | |
| } | |
| const handleCancel = async (id: string) => { | |
| setActionLoading(id); | |
| try { | |
| await friendRequestsApi.cancel(id); | |
| setOutgoing((prev) => prev.filter((r) => r.id !== id)); | |
| toast.success('Request cancelled'); | |
| } catch { | |
| toast.error('Failed to cancel'); | |
| } finally { | |
| setActionLoading(null); | |
| } | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/src/app/dashboard/friends/page.tsx` around lines 89 - 98,
handleCancel currently removes the outgoing request silently; add a success
toast to match the other handlers by calling toast.success(...) after
friendRequestsApi.cancel completes (before or after updating state) so users get
feedback. Update the handleCancel function (references: handleCancel,
friendRequestsApi.cancel, setOutgoing) to invoke toast.success with an
appropriate message (e.g., "Canceled request") in the try block before finally
clearing setActionLoading.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,11 @@ | ||
| import type { Metadata } from 'next'; | ||
| import { DashboardShell } from './dashboard-shell'; | ||
|
|
||
| export const metadata: Metadata = { | ||
| title: 'Dashboard', | ||
| description: 'DevRadar Dashboard - Manage your account and view your coding activity.', | ||
| }; | ||
|
|
||
| export default function DashboardLayout({ children }: { children: React.ReactNode }) { | ||
| return children; | ||
| return <DashboardShell>{children}</DashboardShell>; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The notification preferences are stored in localStorage but never checked in the dashboard-shell.tsx where the toast notifications are triggered. The handlePoke and handleAchievement functions should check these preferences before showing toasts. Consider reading these settings and conditionally showing toasts based on user preferences.