mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-09 18:57:49 +02:00
Accept, reject friends
This commit is contained in:
259
src/components/Friends/FriendProgressItem.tsx
Normal file
259
src/components/Friends/FriendProgressItem.tsx
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
import { useState } from 'preact/hooks';
|
||||||
|
import type { ListFriendsResponse } from './FriendsPage';
|
||||||
|
import { DeleteUserIcon } from '../ReactIcons/DeleteUserIcon';
|
||||||
|
import { pageProgressMessage } from '../../stores/page';
|
||||||
|
import { httpDelete, httpPost } from '../../lib/http';
|
||||||
|
import { useToast } from '../../hooks/use-toast';
|
||||||
|
import { TrashIcon } from '../ReactIcons/TrashIcon';
|
||||||
|
import { AddedUserIcon } from '../ReactIcons/AddedUserIcon';
|
||||||
|
import { AddUserIcon } from '../ReactIcons/AddUserIcon';
|
||||||
|
|
||||||
|
type FriendProgressItemProps = {
|
||||||
|
friend: ListFriendsResponse[0];
|
||||||
|
onShowResourceProgress: (resourceId: string) => void;
|
||||||
|
onReload: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FriendProgressItem(props: FriendProgressItemProps) {
|
||||||
|
const { friend, onShowResourceProgress, onReload } = props;
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
async function deleteFriend(userId: string, successMessage: string) {
|
||||||
|
pageProgressMessage.set('Please wait...');
|
||||||
|
const { response, error } = await httpDelete(
|
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-delete-friend/${userId}`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error || !response) {
|
||||||
|
toast.error(error?.message || 'Something went wrong');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(successMessage);
|
||||||
|
onReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addFriend(userId: string, successMessage: string) {
|
||||||
|
pageProgressMessage.set('Please wait...');
|
||||||
|
const { response, error } = await httpPost(
|
||||||
|
`${import.meta.env.PUBLIC_API_URL}/v1-add-friend/${userId}`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error || !response) {
|
||||||
|
toast.error(error?.message || 'Something went wrong');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success(successMessage);
|
||||||
|
onReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
const roadmaps = (friend.roadmaps || []).sort((a, b) => {
|
||||||
|
return b.done - a.done;
|
||||||
|
});
|
||||||
|
|
||||||
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
const status = friend.status;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`flex h-full min-h-[270px] flex-col overflow-hidden rounded-md border`}
|
||||||
|
key={friend.userId}
|
||||||
|
>
|
||||||
|
<div className={`relative flex items-center gap-3 border-b p-3`}>
|
||||||
|
<img
|
||||||
|
src={
|
||||||
|
friend.avatar
|
||||||
|
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${friend.avatar}`
|
||||||
|
: '/images/default-avatar.png'
|
||||||
|
}
|
||||||
|
alt={friend.name || ''}
|
||||||
|
className="h-8 w-8 rounded-full"
|
||||||
|
/>
|
||||||
|
<div className="inline-grid w-full">
|
||||||
|
<h3 className="truncate font-medium">{friend.name}</h3>
|
||||||
|
<p className="truncate text-sm text-gray-500">{friend.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{friend.status === 'accepted' && (
|
||||||
|
<div className="relative flex grow flex-col space-y-2 p-3">
|
||||||
|
{(showAll ? roadmaps : roadmaps.slice(0, 4)).map((progress) => {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={() => onShowResourceProgress(progress.resourceId)}
|
||||||
|
className="group relative overflow-hidden rounded-md border p-2 hover:border-gray-300 hover:text-black focus:outline-none"
|
||||||
|
key={progress.resourceId}
|
||||||
|
>
|
||||||
|
<span className="relative z-10 flex items-center justify-between text-sm">
|
||||||
|
<span className="inline-grid">
|
||||||
|
<span className={'truncate'}>{progress.title}</span>
|
||||||
|
</span>
|
||||||
|
<span className="ml-1.5 shrink-0 text-xs text-gray-400">
|
||||||
|
{progress.done} / {progress.total}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="absolute inset-0 bg-gray-100 group-hover:bg-gray-200"
|
||||||
|
style={{
|
||||||
|
width: `${(progress.done / progress.total) * 100}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{roadmaps.length > 4 && !showAll && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAll(true)}
|
||||||
|
className={'text-sm text-gray-400 underline'}
|
||||||
|
>
|
||||||
|
+ {roadmaps.length - 4} more
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showAll && (
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAll(false)}
|
||||||
|
className={'text-sm text-gray-400 underline'}
|
||||||
|
>
|
||||||
|
- Show less
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{roadmaps.length === 0 && (
|
||||||
|
<div className="text-sm text-gray-500">No progress</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{friend.status === 'rejected' && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={'flex w-full flex-grow items-center justify-center'}
|
||||||
|
>
|
||||||
|
<span class=" flex flex-col items-center text-red-500">
|
||||||
|
<DeleteUserIcon additionalClasses="mr-2 h-8 w-8 mb-1" />
|
||||||
|
Request Rejected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="flex cursor-default items-center justify-center border-t py-2 text-center text-sm">
|
||||||
|
Changed your mind?{' '}
|
||||||
|
<button
|
||||||
|
className="ml-2 font-medium text-red-700 underline underline-offset-2 hover:text-red-500"
|
||||||
|
onClick={() => {
|
||||||
|
addFriend(friend.userId, 'Friend request accepted').finally(
|
||||||
|
() => {
|
||||||
|
pageProgressMessage.set('');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{friend.status === 'got_rejected' && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={'flex w-full flex-grow items-center justify-center'}
|
||||||
|
>
|
||||||
|
<span class=" flex flex-col items-center text-sm text-red-500">
|
||||||
|
<DeleteUserIcon additionalClasses="mr-2 h-8 w-8 mb-1" />
|
||||||
|
Request Rejected
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="flex cursor-default items-center justify-center border-t py-2.5 text-center text-sm">
|
||||||
|
<button
|
||||||
|
className="ml-2 flex items-center font-medium text-red-700 underline underline-offset-2 hover:text-red-500"
|
||||||
|
onClick={() => {
|
||||||
|
deleteFriend(friend.userId, 'Friend request removed').finally(
|
||||||
|
() => {
|
||||||
|
pageProgressMessage.set('');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon className="mr-1 h-4 w-4" />
|
||||||
|
Delete Request
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{friend.status === 'sent' && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={'flex w-full flex-grow items-center justify-center'}
|
||||||
|
>
|
||||||
|
<span class=" flex flex-col items-center text-green-500">
|
||||||
|
<AddedUserIcon additionalClasses="mr-2 h-8 w-8 mb-1" />
|
||||||
|
Request Sent
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span class="flex cursor-default items-center justify-center border-t py-2 text-center text-sm">
|
||||||
|
<button
|
||||||
|
className="ml-2 flex items-center font-medium text-red-700 underline underline-offset-2 hover:text-red-500"
|
||||||
|
onClick={() => {
|
||||||
|
deleteFriend(
|
||||||
|
friend.userId,
|
||||||
|
'Friend request withdrawn'
|
||||||
|
).finally(() => {
|
||||||
|
pageProgressMessage.set('');
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon className="mr-1 h-4 w-4" />
|
||||||
|
Withdraw Request
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{friend.status === 'received' && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
'flex w-full flex-grow flex-col items-center justify-center px-4'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<AddUserIcon additionalClasses="mr-2 h-10 w-10 mb-1 text-green-500" />
|
||||||
|
<span className="mb-3 text-green-600">Request Received</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
addFriend(friend.userId, 'Friend request accepted').finally(
|
||||||
|
() => {
|
||||||
|
pageProgressMessage.set('');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
className="mb-1 block w-full max-w-[150px] rounded-md bg-black py-1.5 text-sm text-white"
|
||||||
|
>
|
||||||
|
Accept
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
deleteFriend(
|
||||||
|
friend.userId,
|
||||||
|
'Friend request rejected'
|
||||||
|
).finally(() => {
|
||||||
|
pageProgressMessage.set('');
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
className="block w-full max-w-[150px] rounded-md border border-red-500 py-1 text-sm text-red-500"
|
||||||
|
>
|
||||||
|
Reject
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@@ -6,6 +6,8 @@ import { httpGet } from '../../lib/http';
|
|||||||
import type { FriendshipStatus } from '../Befriend';
|
import type { FriendshipStatus } from '../Befriend';
|
||||||
import { useToast } from '../../hooks/use-toast';
|
import { useToast } from '../../hooks/use-toast';
|
||||||
import { EmptyFriends } from './EmptyFriends';
|
import { EmptyFriends } from './EmptyFriends';
|
||||||
|
import { FriendProgressItem } from './FriendProgressItem';
|
||||||
|
import UserIcon from '../../icons/user.svg';
|
||||||
|
|
||||||
type FriendResourceProgress = {
|
type FriendResourceProgress = {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -18,18 +20,35 @@ type FriendResourceProgress = {
|
|||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ListFriendsResponse = {
|
export type ListFriendsResponse = {
|
||||||
userId: string;
|
userId: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
email: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
status: FriendshipStatus;
|
status: FriendshipStatus;
|
||||||
roadmaps: FriendResourceProgress[];
|
roadmaps: FriendResourceProgress[];
|
||||||
bestPractices: FriendResourceProgress[];
|
bestPractices: FriendResourceProgress[];
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
|
type GroupingType = {
|
||||||
|
label: string;
|
||||||
|
value: 'active' | 'requests' | 'sent';
|
||||||
|
statuses: FriendshipStatus[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const groupingTypes: GroupingType[] = [
|
||||||
|
{ label: 'Active', value: 'active', statuses: ['accepted'] },
|
||||||
|
{ label: 'Requests', value: 'requests', statuses: ['received', 'rejected'] },
|
||||||
|
{ label: 'Sent', value: 'sent', statuses: ['sent', 'got_rejected'] },
|
||||||
|
];
|
||||||
|
|
||||||
export function FriendsPage() {
|
export function FriendsPage() {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [friends, setFriends] = useState<ListFriendsResponse>([]);
|
const [friends, setFriends] = useState<ListFriendsResponse>([]);
|
||||||
|
const [selectedGrouping, setSelectedGrouping] =
|
||||||
|
useState<GroupingType['value']>('active');
|
||||||
|
|
||||||
async function loadFriends() {
|
async function loadFriends() {
|
||||||
const { response, error } = await httpGet<ListFriendsResponse>(
|
const { response, error } = await httpGet<ListFriendsResponse>(
|
||||||
@@ -47,6 +66,7 @@ export function FriendsPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadFriends().finally(() => {
|
loadFriends().finally(() => {
|
||||||
pageProgressMessage.set('');
|
pageProgressMessage.set('');
|
||||||
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -60,17 +80,100 @@ export function FriendsPage() {
|
|||||||
return <EmptyFriends befriendUrl={befriendUrl} />;
|
return <EmptyFriends befriendUrl={befriendUrl} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedGroupingType = groupingTypes.find(
|
||||||
|
(grouping) => grouping.value === selectedGrouping
|
||||||
|
);
|
||||||
|
|
||||||
|
const filteredFriends = friends.filter((friend) =>
|
||||||
|
selectedGroupingType?.statuses.includes(friend.status)
|
||||||
|
);
|
||||||
|
|
||||||
|
const receivedRequests = friends.filter(
|
||||||
|
(friend) => friend.status === 'received'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!friends?.length) {
|
||||||
|
return <EmptyFriends befriendUrl={befriendUrl} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
<span className={'text-sm text-gray-400'}>
|
<div className="flex items-center gap-2">
|
||||||
You have 4 active friends
|
{groupingTypes.map((grouping) => {
|
||||||
|
let requestCount = 0;
|
||||||
|
if (grouping.value === 'requests') {
|
||||||
|
requestCount = receivedRequests.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className={`relative flex items-center justify-center rounded-md border p-1 px-3 text-sm ${
|
||||||
|
selectedGrouping === grouping.value
|
||||||
|
? ' border-gray-400 bg-gray-200 '
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
|
onClick={() => setSelectedGrouping(grouping.value)}
|
||||||
|
>
|
||||||
|
{grouping.label}
|
||||||
|
{requestCount > 0 && (
|
||||||
|
<span className="ml-1.5 inline-flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-[10px] text-white">
|
||||||
|
{requestCount}
|
||||||
</span>
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
<button class="flex items-center justify-center gap-1.5 rounded-md border border-gray-400 bg-gray-50 p-1 px-2 text-sm hover:border-gray-500 hover:bg-gray-100">
|
<button class="flex items-center justify-center gap-1.5 rounded-md border border-gray-400 bg-gray-50 p-1 px-2 text-sm hover:border-gray-500 hover:bg-gray-100">
|
||||||
<AddUserIcon additionalClasses="w-4 h-4" />
|
<AddUserIcon additionalClasses="w-4 h-4" />
|
||||||
Invite Friends
|
Invite Friends
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{filteredFriends.length > 0 && (
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
{filteredFriends.map((friend) => (
|
||||||
|
<FriendProgressItem
|
||||||
|
friend={friend}
|
||||||
|
onShowResourceProgress={(resourceId) => {}}
|
||||||
|
key={friend.userId}
|
||||||
|
onReload={() => {
|
||||||
|
pageProgressMessage.set('Reloading friends ..');
|
||||||
|
loadFriends().finally(() => {
|
||||||
|
pageProgressMessage.set('');
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filteredFriends.length === 0 && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12">
|
||||||
|
<img
|
||||||
|
src={UserIcon}
|
||||||
|
alt="Empty Friends"
|
||||||
|
className="mb-3 w-12 opacity-20"
|
||||||
|
/>
|
||||||
|
<h2 className="text-lg font-semibold">
|
||||||
|
{selectedGrouping === 'active' && 'No friends yet'}
|
||||||
|
{selectedGrouping === 'sent' && 'No requests sent'}
|
||||||
|
{selectedGrouping === 'requests' && 'No requests received'}
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Invite your friends to join you on Roadmap
|
||||||
|
</p>
|
||||||
|
<button className="mt-4 flex items-center justify-center gap-1.5 rounded-md border border-gray-400 bg-gray-50 p-1 px-2 text-sm hover:border-gray-500 hover:bg-gray-100">
|
||||||
|
<AddUserIcon additionalClasses="w-4 h-4" />
|
||||||
|
Invite Friends
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user