mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-30 20:49:49 +02:00
Add friends listing
This commit is contained in:
@@ -21,6 +21,16 @@ const sidebarLinks = [
|
|||||||
classes: 'h-3 w-4',
|
classes: 'h-3 w-4',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
href: '/account/friends',
|
||||||
|
title: 'Friends',
|
||||||
|
id: 'friends',
|
||||||
|
isNew: true,
|
||||||
|
icon: {
|
||||||
|
glyph: 'users',
|
||||||
|
classes: 'h-4 w-4',
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
href: '/account/road-card',
|
href: '/account/road-card',
|
||||||
title: 'Card',
|
title: 'Card',
|
||||||
@@ -31,16 +41,6 @@ const sidebarLinks = [
|
|||||||
classes: 'h-4 w-4',
|
classes: 'h-4 w-4',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// href: '/account/friends',
|
|
||||||
// title: 'Friends',
|
|
||||||
// id: 'friends',
|
|
||||||
// isNew: true,
|
|
||||||
// icon: {
|
|
||||||
// glyph: 'users',
|
|
||||||
// classes: 'h-4 w-4',
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
href: '/account/update-profile',
|
href: '/account/update-profile',
|
||||||
title: 'Profile',
|
title: 'Profile',
|
||||||
|
@@ -17,6 +17,8 @@ type FriendProgressItemProps = {
|
|||||||
export function FriendProgressItem(props: FriendProgressItemProps) {
|
export function FriendProgressItem(props: FriendProgressItemProps) {
|
||||||
const { friend, onShowResourceProgress, onReload } = props;
|
const { friend, onShowResourceProgress, onReload } = props;
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const [isConfirming, setIsConfirming] =
|
||||||
|
useState<ListFriendsResponse[0]['status']>();
|
||||||
|
|
||||||
async function deleteFriend(userId: string, successMessage: string) {
|
async function deleteFriend(userId: string, successMessage: string) {
|
||||||
pageProgressMessage.set('Please wait...');
|
pageProgressMessage.set('Please wait...');
|
||||||
@@ -79,54 +81,95 @@ export function FriendProgressItem(props: FriendProgressItemProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{friend.status === 'accepted' && (
|
{friend.status === 'accepted' && (
|
||||||
<div className="relative flex grow flex-col space-y-2 p-3">
|
<>
|
||||||
{(showAll ? roadmaps : roadmaps.slice(0, 4)).map((progress) => {
|
<div className="relative flex grow flex-col space-y-2 p-3">
|
||||||
return (
|
{(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
|
<button
|
||||||
onClick={() => onShowResourceProgress(progress.resourceId)}
|
onClick={() => setShowAll(true)}
|
||||||
className="group relative overflow-hidden rounded-md border p-2 hover:border-gray-300 hover:text-black focus:outline-none"
|
className={'text-xs text-gray-400 underline'}
|
||||||
key={progress.resourceId}
|
|
||||||
>
|
>
|
||||||
<span className="relative z-10 flex items-center justify-between text-sm">
|
+ {roadmaps.length - 4} more
|
||||||
<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>
|
</button>
|
||||||
);
|
)}
|
||||||
})}
|
|
||||||
|
|
||||||
{roadmaps.length > 4 && !showAll && (
|
{showAll && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowAll(true)}
|
onClick={() => setShowAll(false)}
|
||||||
className={'text-sm text-gray-400 underline'}
|
className={'text-sm text-gray-400 underline'}
|
||||||
>
|
>
|
||||||
+ {roadmaps.length - 4} more
|
- Show less
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showAll && (
|
{roadmaps.length === 0 && (
|
||||||
<button
|
<div className="text-sm text-gray-500">No progress</div>
|
||||||
onClick={() => setShowAll(false)}
|
)}
|
||||||
className={'text-sm text-gray-400 underline'}
|
</div>
|
||||||
>
|
<>
|
||||||
- Show less
|
{isConfirming !== 'accepted' && (
|
||||||
</button>
|
<button
|
||||||
)}
|
className="flex w-full items-center justify-center border-t py-2 text-sm font-medium text-red-700 hover:bg-red-50/50 hover:text-red-500"
|
||||||
|
onClick={() => {
|
||||||
|
setIsConfirming('accepted');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TrashIcon className="mr-1 h-4 w-4" />
|
||||||
|
Remove Friend
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
{roadmaps.length === 0 && (
|
{isConfirming === 'accepted' && (
|
||||||
<div className="text-sm text-gray-500">No progress</div>
|
<span className="flex w-full items-center justify-center border-t py-2 text-sm text-red-700">
|
||||||
)}
|
Are you sure?{' '}
|
||||||
</div>
|
<button
|
||||||
|
className="ml-2 font-medium text-red-700 underline underline-offset-2 hover:text-red-500"
|
||||||
|
onClick={() => {
|
||||||
|
deleteFriend(friend.userId, 'Friend removed').finally(
|
||||||
|
() => {
|
||||||
|
pageProgressMessage.set('');
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Yes
|
||||||
|
</button>{' '}
|
||||||
|
<button
|
||||||
|
className="ml-2 font-medium text-red-700 underline underline-offset-2 hover:text-red-500"
|
||||||
|
onClick={() => {
|
||||||
|
setIsConfirming(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
No
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{friend.status === 'rejected' && (
|
{friend.status === 'rejected' && (
|
||||||
|
@@ -9,6 +9,7 @@ import { EmptyFriends } from './EmptyFriends';
|
|||||||
import { FriendProgressItem } from './FriendProgressItem';
|
import { FriendProgressItem } from './FriendProgressItem';
|
||||||
import UserIcon from '../../icons/user.svg';
|
import UserIcon from '../../icons/user.svg';
|
||||||
import { UserProgressModal } from '../UserProgress/UserProgressModal';
|
import { UserProgressModal } from '../UserProgress/UserProgressModal';
|
||||||
|
import { InviteFriendPopup } from './InviteFriendPopup';
|
||||||
|
|
||||||
type FriendResourceProgress = {
|
type FriendResourceProgress = {
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -46,6 +47,8 @@ const groupingTypes: GroupingType[] = [
|
|||||||
export function FriendsPage() {
|
export function FriendsPage() {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
|
const [showInviteFriendPopup, setShowInviteFriendPopup] = useState(false);
|
||||||
|
|
||||||
const [showFriendProgress, setShowFriendProgress] = useState<{
|
const [showFriendProgress, setShowFriendProgress] = useState<{
|
||||||
resourceId: string;
|
resourceId: string;
|
||||||
friend: ListFriendsResponse[0];
|
friend: ListFriendsResponse[0];
|
||||||
@@ -108,6 +111,13 @@ export function FriendsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{showInviteFriendPopup && (
|
||||||
|
<InviteFriendPopup
|
||||||
|
befriendUrl={befriendUrl}
|
||||||
|
onClose={() => setShowInviteFriendPopup(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{showFriendProgress && (
|
{showFriendProgress && (
|
||||||
<UserProgressModal
|
<UserProgressModal
|
||||||
userId={showFriendProgress.friend.userId}
|
userId={showFriendProgress.friend.userId}
|
||||||
@@ -117,7 +127,7 @@ export function FriendsPage() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex flex-col items-stretch justify-between gap-2 sm:flex-row sm:items-center sm:gap-0">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{groupingTypes.map((grouping) => {
|
{groupingTypes.map((grouping) => {
|
||||||
let requestCount = 0;
|
let requestCount = 0;
|
||||||
@@ -131,7 +141,7 @@ export function FriendsPage() {
|
|||||||
selectedGrouping === grouping.value
|
selectedGrouping === grouping.value
|
||||||
? ' border-gray-400 bg-gray-200 '
|
? ' border-gray-400 bg-gray-200 '
|
||||||
: ''
|
: ''
|
||||||
}`}
|
} w-full sm:w-auto`}
|
||||||
onClick={() => setSelectedGrouping(grouping.value)}
|
onClick={() => setSelectedGrouping(grouping.value)}
|
||||||
>
|
>
|
||||||
{grouping.label}
|
{grouping.label}
|
||||||
@@ -144,14 +154,19 @@ export function FriendsPage() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</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
|
||||||
|
onClick={() => {
|
||||||
|
setShowInviteFriendPopup(true);
|
||||||
|
}}
|
||||||
|
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 && (
|
{filteredFriends.length > 0 && (
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
{filteredFriends.map((friend) => (
|
{filteredFriends.map((friend) => (
|
||||||
<FriendProgressItem
|
<FriendProgressItem
|
||||||
friend={friend}
|
friend={friend}
|
||||||
@@ -188,7 +203,12 @@ export function FriendsPage() {
|
|||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
Invite your friends to join you on Roadmap
|
Invite your friends to join you on Roadmap
|
||||||
</p>
|
</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">
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setShowInviteFriendPopup(true);
|
||||||
|
}}
|
||||||
|
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" />
|
<AddUserIcon additionalClasses="w-4 h-4" />
|
||||||
Invite Friends
|
Invite Friends
|
||||||
</button>
|
</button>
|
||||||
|
64
src/components/Friends/InviteFriendPopup.tsx
Normal file
64
src/components/Friends/InviteFriendPopup.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { useEffect, useRef } from 'preact/hooks';
|
||||||
|
import { useOutsideClick } from '../../hooks/use-outside-click';
|
||||||
|
import CopyIcon from '../../icons/copy.svg';
|
||||||
|
import { useCopyText } from '../../hooks/use-copy-text';
|
||||||
|
|
||||||
|
type InviteFriendPopupProps = {
|
||||||
|
befriendUrl: string;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function InviteFriendPopup(props: InviteFriendPopupProps) {
|
||||||
|
const { onClose, befriendUrl } = props;
|
||||||
|
|
||||||
|
const { isCopied, copyText } = useCopyText();
|
||||||
|
|
||||||
|
const popupBodyRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleClosePopup = () => {
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
useOutsideClick(popupBodyRef, handleClosePopup);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="popup fixed left-0 right-0 top-0 z-50 flex h-full items-center justify-center overflow-y-auto overflow-x-hidden bg-black/50">
|
||||||
|
<div class="relative h-full w-full max-w-md p-4 md:h-auto">
|
||||||
|
<div
|
||||||
|
ref={popupBodyRef}
|
||||||
|
class="popup-body relative rounded-lg bg-white p-4 shadow"
|
||||||
|
>
|
||||||
|
<h3 class="mb-1.5 text-xl font-medium sm:text-2xl">Invite URL</h3>
|
||||||
|
<p className="mb-3 hidden text-sm leading-none text-gray-400 sm:block">
|
||||||
|
Share the link below with your friends to invite them.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-4 flex flex-col gap-2 sm:mt-4">
|
||||||
|
<input
|
||||||
|
readOnly={true}
|
||||||
|
className="mt-2 block w-full rounded-md border border-gray-300 px-3 py-2 outline-none placeholder:text-gray-400 focus:border-gray-400"
|
||||||
|
value={befriendUrl}
|
||||||
|
onClick={(e) => {
|
||||||
|
e?.target?.select();
|
||||||
|
copyText(befriendUrl);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class={`flex items-center justify-center gap-1 rounded-md border-0 px-3 py-2.5 text-sm text-black ${
|
||||||
|
isCopied
|
||||||
|
? 'bg-green-300 hover:bg-green-300'
|
||||||
|
: 'bg-gray-200 hover:bg-gray-300'
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
copyText(befriendUrl);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={CopyIcon} className="h-4 w-4" alt="Invite Friends" />
|
||||||
|
{isCopied ? 'Copied' : 'Copy URL'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@@ -30,6 +30,14 @@ import Icon from '../AstroIcon.astro';
|
|||||||
Profile
|
Profile
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class='px-1'>
|
||||||
|
<a
|
||||||
|
href='/account/friends'
|
||||||
|
class='block rounded px-4 py-2 text-sm font-medium text-slate-100 hover:bg-slate-700'
|
||||||
|
>
|
||||||
|
Friends
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class='px-1'>
|
<li class='px-1'>
|
||||||
<a
|
<a
|
||||||
href='/team'
|
href='/team'
|
||||||
|
Reference in New Issue
Block a user