Add friends listing

pull/4317/head
Kamran Ahmed 1 year ago
parent 92295a7906
commit b69889cc29
  1. 20
      src/components/AccountSidebar.astro
  2. 127
      src/components/Friends/FriendProgressItem.tsx
  3. 30
      src/components/Friends/FriendsPage.tsx
  4. 64
      src/components/Friends/InviteFriendPopup.tsx
  5. 8
      src/components/Navigation/AccountDropdown.astro

@ -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>

@ -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'

Loading…
Cancel
Save