mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-30 04:30:01 +02:00
Add friends listing
This commit is contained in:
@@ -21,6 +21,16 @@ const sidebarLinks = [
|
||||
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',
|
||||
title: 'Card',
|
||||
@@ -31,16 +41,6 @@ const sidebarLinks = [
|
||||
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',
|
||||
title: 'Profile',
|
||||
|
@@ -17,6 +17,8 @@ type FriendProgressItemProps = {
|
||||
export function FriendProgressItem(props: FriendProgressItemProps) {
|
||||
const { friend, onShowResourceProgress, onReload } = props;
|
||||
const toast = useToast();
|
||||
const [isConfirming, setIsConfirming] =
|
||||
useState<ListFriendsResponse[0]['status']>();
|
||||
|
||||
async function deleteFriend(userId: string, successMessage: string) {
|
||||
pageProgressMessage.set('Please wait...');
|
||||
@@ -79,54 +81,95 @@ export function FriendProgressItem(props: FriendProgressItemProps) {
|
||||
</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 (
|
||||
<>
|
||||
<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={() => 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}
|
||||
onClick={() => setShowAll(true)}
|
||||
className={'text-xs text-gray-400 underline'}
|
||||
>
|
||||
<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}%`,
|
||||
}}
|
||||
/>
|
||||
+ {roadmaps.length - 4} more
|
||||
</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>
|
||||
)}
|
||||
|
||||
{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>
|
||||
<>
|
||||
{isConfirming !== 'accepted' && (
|
||||
<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 && (
|
||||
<div className="text-sm text-gray-500">No progress</div>
|
||||
)}
|
||||
</div>
|
||||
{isConfirming === 'accepted' && (
|
||||
<span className="flex w-full items-center justify-center border-t py-2 text-sm text-red-700">
|
||||
Are you sure?{' '}
|
||||
<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' && (
|
||||
|
@@ -9,6 +9,7 @@ import { EmptyFriends } from './EmptyFriends';
|
||||
import { FriendProgressItem } from './FriendProgressItem';
|
||||
import UserIcon from '../../icons/user.svg';
|
||||
import { UserProgressModal } from '../UserProgress/UserProgressModal';
|
||||
import { InviteFriendPopup } from './InviteFriendPopup';
|
||||
|
||||
type FriendResourceProgress = {
|
||||
updatedAt: string;
|
||||
@@ -46,6 +47,8 @@ const groupingTypes: GroupingType[] = [
|
||||
export function FriendsPage() {
|
||||
const toast = useToast();
|
||||
|
||||
const [showInviteFriendPopup, setShowInviteFriendPopup] = useState(false);
|
||||
|
||||
const [showFriendProgress, setShowFriendProgress] = useState<{
|
||||
resourceId: string;
|
||||
friend: ListFriendsResponse[0];
|
||||
@@ -108,6 +111,13 @@ export function FriendsPage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{showInviteFriendPopup && (
|
||||
<InviteFriendPopup
|
||||
befriendUrl={befriendUrl}
|
||||
onClose={() => setShowInviteFriendPopup(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showFriendProgress && (
|
||||
<UserProgressModal
|
||||
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">
|
||||
{groupingTypes.map((grouping) => {
|
||||
let requestCount = 0;
|
||||
@@ -131,7 +141,7 @@ export function FriendsPage() {
|
||||
selectedGrouping === grouping.value
|
||||
? ' border-gray-400 bg-gray-200 '
|
||||
: ''
|
||||
}`}
|
||||
} w-full sm:w-auto`}
|
||||
onClick={() => setSelectedGrouping(grouping.value)}
|
||||
>
|
||||
{grouping.label}
|
||||
@@ -144,14 +154,19 @@ export function FriendsPage() {
|
||||
);
|
||||
})}
|
||||
</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" />
|
||||
Invite Friends
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{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) => (
|
||||
<FriendProgressItem
|
||||
friend={friend}
|
||||
@@ -188,7 +203,12 @@ export function FriendsPage() {
|
||||
<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">
|
||||
<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" />
|
||||
Invite Friends
|
||||
</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
|
||||
</a>
|
||||
</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'>
|
||||
<a
|
||||
href='/team'
|
||||
|
Reference in New Issue
Block a user