|
|
|
import { useEffect, useState } from 'react';
|
|
|
|
import { useToast } from '../../hooks/use-toast';
|
|
|
|
import { UserItem } from './UserItem';
|
|
|
|
import { Check, Copy, Group, UserPlus2, Users2 } from 'lucide-react';
|
|
|
|
import { httpGet } from '../../lib/http';
|
|
|
|
import { getUser } from '../../lib/jwt.ts';
|
|
|
|
import { useCopyText } from '../../hooks/use-copy-text.ts';
|
|
|
|
import { cn } from '../../lib/classname.ts';
|
|
|
|
|
|
|
|
export type FriendshipStatus =
|
|
|
|
| 'none'
|
|
|
|
| 'sent'
|
|
|
|
| 'received'
|
|
|
|
| 'accepted'
|
|
|
|
| 'rejected'
|
|
|
|
| 'got_rejected';
|
|
|
|
|
|
|
|
type FriendResourceProgress = {
|
|
|
|
updatedAt: string;
|
|
|
|
title: string;
|
|
|
|
resourceId: string;
|
|
|
|
resourceType: string;
|
|
|
|
learning: number;
|
|
|
|
skipped: number;
|
|
|
|
done: number;
|
|
|
|
total: number;
|
|
|
|
};
|
|
|
|
|
|
|
|
export type ListFriendsResponse = {
|
|
|
|
userId: string;
|
|
|
|
name: string;
|
|
|
|
email: string;
|
|
|
|
avatar: string;
|
|
|
|
status: FriendshipStatus;
|
|
|
|
roadmaps: FriendResourceProgress[];
|
|
|
|
bestPractices: FriendResourceProgress[];
|
|
|
|
}[];
|
|
|
|
|
|
|
|
type ShareFriendListProps = {
|
|
|
|
setFriends: (friends: ListFriendsResponse) => void;
|
|
|
|
friends: ListFriendsResponse;
|
|
|
|
sharedFriendIds: string[];
|
|
|
|
setSharedFriendIds: (friendIds: string[]) => void;
|
|
|
|
};
|
|
|
|
|
|
|
|
export function ShareFriendList(props: ShareFriendListProps) {
|
|
|
|
const userId = getUser()?.id!;
|
|
|
|
const { setFriends, friends, sharedFriendIds, setSharedFriendIds } = props;
|
|
|
|
const toast = useToast();
|
|
|
|
|
|
|
|
const { isCopied, copyText } = useCopyText();
|
|
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
const [isAddingFriend, setIsAddingFriend] = useState(false);
|
|
|
|
|
|
|
|
async function loadFriends() {
|
|
|
|
if (friends.length > 0) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setIsLoading(true);
|
|
|
|
const { response, error } = await httpGet<ListFriendsResponse>(
|
|
|
|
`${import.meta.env.PUBLIC_API_URL}/v1-list-friends`,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (error || !response) {
|
|
|
|
toast.error(error?.message || 'Something went wrong');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
setFriends(response.filter((friend) => friend.status === 'accepted'));
|
|
|
|
}
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
loadFriends().finally(() => {
|
|
|
|
setIsLoading(false);
|
|
|
|
});
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
const loadingFriends = isLoading && (
|
|
|
|
<ul className="mt-2 grid grid-cols-3 gap-1.5">
|
|
|
|
{[...Array(3)].map((_, idx) => (
|
|
|
|
<li
|
|
|
|
key={idx}
|
|
|
|
className="flex animate-pulse items-center gap-2.5 rounded-md border p-2"
|
|
|
|
>
|
|
|
|
<div className="relative top-[1px] h-10 w-10 shrink-0 rounded-full bg-gray-200" />
|
|
|
|
<div className="inline-grid w-full">
|
|
|
|
<div className="h-5 w-2/4 rounded bg-gray-200" />
|
|
|
|
<div className="mt-1 h-5 w-3/4 rounded bg-gray-200" />
|
|
|
|
</div>
|
|
|
|
</li>
|
|
|
|
))}
|
|
|
|
</ul>
|
|
|
|
);
|
|
|
|
|
|
|
|
const isDev = import.meta.env.DEV;
|
|
|
|
const baseWebUrl = isDev ? 'http://localhost:3000' : 'https://roadmap.sh';
|
|
|
|
const befriendUrl = `${baseWebUrl}/befriend?u=${userId}`;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{(friends.length > 0 || isLoading) && (
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
<p className="text-sm">Select Friends to share the roadmap with</p>
|
|
|
|
|
|
|
|
<label className="flex items-center gap-2 text-sm">
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
checked={sharedFriendIds.length === friends.length}
|
|
|
|
onChange={(e) => {
|
|
|
|
if (e.target.checked) {
|
|
|
|
setSharedFriendIds(friends.map((f) => f.userId));
|
|
|
|
} else {
|
|
|
|
setSharedFriendIds([]);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<span className="text-sm">Select all</span>
|
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{loadingFriends}
|
|
|
|
{friends.length > 0 && !isLoading && (
|
|
|
|
<>
|
|
|
|
<ul className="mt-2 grid grid-cols-3 gap-1.5">
|
|
|
|
{friends.map((friend) => {
|
|
|
|
const isSelected = sharedFriendIds?.includes(friend.userId);
|
|
|
|
return (
|
|
|
|
<li key={friend.userId}>
|
|
|
|
<UserItem
|
|
|
|
user={{
|
|
|
|
name: friend.name,
|
|
|
|
avatar: friend.avatar,
|
|
|
|
email: friend.email,
|
|
|
|
}}
|
|
|
|
isSelected={isSelected}
|
|
|
|
onClick={() => {
|
|
|
|
if (isSelected) {
|
|
|
|
setSharedFriendIds(
|
|
|
|
sharedFriendIds.filter((id) => id !== friend.userId),
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
setSharedFriendIds([...sharedFriendIds, friend.userId]);
|
|
|
|
}
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</li>
|
|
|
|
);
|
|
|
|
})}
|
|
|
|
</ul>
|
|
|
|
{!isAddingFriend && (
|
|
|
|
<p className="mt-6 text-sm text-gray-600">
|
|
|
|
Don't see a Friend?{' '}
|
|
|
|
<button
|
|
|
|
onClick={() => {
|
|
|
|
setIsAddingFriend(true);
|
|
|
|
}}
|
|
|
|
className="font-semibold text-gray-900 underline"
|
|
|
|
>
|
|
|
|
Add them
|
|
|
|
</button>
|
|
|
|
</p>
|
|
|
|
)}
|
|
|
|
{isAddingFriend && (
|
|
|
|
<div className="-mx-4 -mb-4 mt-6 border-t bg-gray-50 px-4 py-4">
|
|
|
|
<p className="mb-1.5 flex items-center gap-1 text-sm text-gray-800">
|
|
|
|
<UserPlus2 className="text-gray-500" size="20px" />
|
|
|
|
Share the link below with your friends to invite them
|
|
|
|
</p>
|
|
|
|
<div className="relative">
|
|
|
|
<input
|
|
|
|
readOnly
|
|
|
|
type="text"
|
|
|
|
value={befriendUrl}
|
|
|
|
onClick={(e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
(e.target as HTMLInputElement).select();
|
|
|
|
copyText(befriendUrl);
|
|
|
|
}}
|
|
|
|
className={cn(
|
|
|
|
'w-full rounded-md border px-2 py-2 text-sm focus:shadow-none focus:outline-0',
|
|
|
|
{
|
|
|
|
'border-green-400 bg-green-50': isCopied,
|
|
|
|
},
|
|
|
|
)}
|
|
|
|
/>
|
|
|
|
<button
|
|
|
|
onClick={() => copyText(befriendUrl)}
|
|
|
|
className="absolute bottom-0 right-0 top-0 flex items-center px-2.5"
|
|
|
|
>
|
|
|
|
{isCopied ? (
|
|
|
|
<span className="flex items-center gap-1 text-sm font-medium text-green-600">
|
|
|
|
<Check className="text-green-600" size="18px" /> Copied
|
|
|
|
</span>
|
|
|
|
) : (
|
|
|
|
<Copy className="text-gray-400" size="18px" />
|
|
|
|
)}
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
|
|
|
|
{friends.length === 0 && !isLoading && (
|
|
|
|
<div className="flex h-full flex-grow flex-col items-center justify-center rounded-md border bg-gray-50 text-center">
|
|
|
|
<Users2 className="mb-3 h-10 w-10 text-gray-300" />
|
|
|
|
<p className="font-semibold text-gray-500">
|
|
|
|
You do not have any friends yet. <br />{' '}
|
|
|
|
<a
|
|
|
|
target="_blank"
|
|
|
|
className="underline underline-offset-2"
|
|
|
|
href={`/account/friends`}
|
|
|
|
>
|
|
|
|
Invite your friends to share roadmaps with.
|
|
|
|
</a>
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}
|