Add friend page

pull/4317/head
Kamran Ahmed 1 year ago
parent 9ed60d836a
commit 0a5eeae68c
  1. 2
      src/components/AuthenticationFlow/GitHubButton.tsx
  2. 2
      src/components/AuthenticationFlow/GoogleButton.tsx
  3. 2
      src/components/AuthenticationFlow/LinkedInButton.tsx
  4. 226
      src/components/Befriend.tsx
  5. 24
      src/components/Friends/EmptyFriends.tsx
  6. 11
      src/components/Friends/FriendsPage.tsx
  7. 27
      src/components/ReactIcons/DeleteUserIcon.tsx
  8. 1
      src/components/Toast.tsx
  9. 14
      src/pages/befriend.astro

@ -91,7 +91,7 @@ export function GitHubButton(props: GitHubButtonProps) {
// the user was on before they clicked the social login button // the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) { if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = const pagePath =
window.location.pathname === '/respond-invite' ['/respond-invite', '/befriend'].includes(window.location.pathname)
? window.location.pathname + window.location.search ? window.location.pathname + window.location.search
: window.location.pathname; : window.location.pathname;

@ -86,7 +86,7 @@ export function GoogleButton(props: GoogleButtonProps) {
// the user was on before they clicked the social login button // the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) { if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = const pagePath =
window.location.pathname === '/respond-invite' ['/respond-invite', '/befriend'].includes(window.location.pathname)
? window.location.pathname + window.location.search ? window.location.pathname + window.location.search
: window.location.pathname; : window.location.pathname;

@ -86,7 +86,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
// the user was on before they clicked the social login button // the user was on before they clicked the social login button
if (!['/login', '/signup'].includes(window.location.pathname)) { if (!['/login', '/signup'].includes(window.location.pathname)) {
const pagePath = const pagePath =
window.location.pathname === '/respond-invite' ['/respond-invite', '/befriend'].includes(window.location.pathname)
? window.location.pathname + window.location.search ? window.location.pathname + window.location.search
: window.location.pathname; : window.location.pathname;

@ -0,0 +1,226 @@
import { useEffect, useState } from 'preact/hooks';
import { httpDelete, httpGet, httpPatch, httpPost } from '../lib/http';
import ErrorIcon from '../icons/error.svg';
import { pageProgressMessage } from '../stores/page';
import { isLoggedIn } from '../lib/jwt';
import { showLoginPopup } from '../lib/popup';
import { getUrlParams } from '../lib/browser';
import { CheckIcon } from './ReactIcons/CheckIcon';
import { DeleteUserIcon } from './ReactIcons/DeleteUserIcon';
import { useToast } from '../hooks/use-toast';
import { useAuth } from '../hooks/use-auth';
export type FriendshipStatus =
| 'none'
| 'sent'
| 'received'
| 'accepted'
| 'rejected'
| 'got_rejected';
type UserResponse = {
id: string;
links: Record<string, string>;
avatar: string;
name: string;
status: FriendshipStatus;
};
export function Befriend() {
const { u: inviteId } = getUrlParams();
const toast = useToast();
const currentUser = useAuth();
const [isConfirming, setIsConfirming] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState('');
const [user, setUser] = useState<UserResponse>();
const isAuthenticated = isLoggedIn();
async function loadUser(userId: string) {
const { response, error } = await httpGet<UserResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-friend/${userId}`
);
if (error || !response) {
setError(error?.message || 'Something went wrong');
return;
}
setUser(response);
}
useEffect(() => {
if (inviteId) {
loadUser(inviteId).finally(() => {
pageProgressMessage.set('');
setIsLoading(false);
});
} else {
setIsLoading(false);
setError('Missing invite ID in URL');
pageProgressMessage.set('');
}
}, [inviteId]);
async function addFriend(userId: string, successMessage: string) {
pageProgressMessage.set('Please wait...');
setError('');
const { response, error } = await httpPost<UserResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-add-friend/${userId}`,
{}
);
if (error || !response) {
setError(error?.message || 'Something went wrong');
return;
}
setUser(response);
toast.success(successMessage);
}
async function deleteFriend(userId: string, successMessage: string) {
pageProgressMessage.set('Please wait...');
setError('');
const { response, error } = await httpDelete<UserResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-delete-friend/${userId}`,
{}
);
if (error || !response) {
setError(error?.message || 'Something went wrong');
return;
}
setUser(response);
toast.success(successMessage);
}
if (isLoading) {
return null;
}
if (!user) {
return (
<div className="container text-center">
<img
alt={'error'}
src={ErrorIcon}
className="mx-auto mb-4 mt-24 w-20 opacity-20"
/>
<h2 className={'mb-1 text-2xl font-bold'}>Error</h2>
<p class="mb-4 text-base leading-6 text-gray-600">
{error || 'There was a problem, please try again.'}
</p>
<div>
<a
href="/"
className="flex-grow cursor-pointer rounded-lg bg-gray-200 px-3 py-2 text-center"
>
Back to home
</a>
</div>
</div>
);
}
const userAvatar = user.avatar
? `${import.meta.env.PUBLIC_AVATAR_BASE_URL}/${user.avatar}`
: '/images/default-avatar.png';
const isMe = currentUser?.id === user.id;
return (
<div className="container max-w-[400px] text-center">
<img
alt={'join team'}
src={userAvatar}
className="mx-auto mb-4 mt-24 w-28 rounded-full"
/>
<h2 className={'mb-1 text-3xl font-bold'}>{user.name}</h2>
<p class="mb-6 text-base leading-6 text-gray-600">
After you add {user.name} as a friend, you will be able to view each
other's skills and progress.
</p>
<div class="mx-auto w-full duration-500 sm:max-w-md">
<div class="flex w-full flex-col items-center gap-2">
{user.status === 'none' && (
<button
disabled={isMe}
onClick={() => {
if (!isAuthenticated) {
return showLoginPopup();
}
addFriend(user.id, 'Friend request sent').finally(() => {
pageProgressMessage.set('');
});
}}
type="button"
class="w-full flex-grow cursor-pointer rounded-lg bg-black px-3 py-2 text-center text-white disabled:cursor-not-allowed disabled:opacity-40"
>
{isMe ? "You can't add yourself" : 'Add Friend'}
</button>
)}
{user.status === 'sent' && (
<>
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-gray-300 bg-gray-100 px-3 py-2 text-center text-black">
<CheckIcon additionalClasses="mr-2 h-4 w-4" />
Request Sent
</span>
{!isConfirming && (
<button
onClick={() => {
setIsConfirming(true);
}}
type="button"
class="flex w-full flex-grow cursor-pointer items-center justify-center rounded-lg border border-red-600 bg-red-600 px-3 py-2 text-center text-white hover:bg-red-700"
>
<DeleteUserIcon additionalClasses="mr-2 h-[19px] w-[19px]" />
Withdraw Request
</button>
)}
{isConfirming && (
<span class="flex w-full flex-grow cursor-default items-center justify-center rounded-lg border border-red-600 px-3 py-2.5 text-center text-sm text-red-600">
Are you sure?{' '}
<button
className="ml-2 text-red-700 underline"
onClick={() => {
deleteFriend(user.id, 'Friend request withdrawn').finally(
() => {
pageProgressMessage.set('');
}
);
}}
>
Yes
</button>{' '}
<button
onClick={() => {
setIsConfirming(false);
}}
className="ml-2 text-red-600 underline"
>
No
</button>
</span>
)}
</>
)}
</div>
</div>
{error && (
<p className="mt-2 rounded-lg bg-red-100 p-2 text-red-700">{error}</p>
)}
</div>
);
}

@ -1,27 +1,29 @@
import UserPlusIcon from '../../icons/user-plus.svg'; import UserPlusIcon from '../../icons/user-plus.svg';
import CopyIcon from '../../icons/copy.svg'; import CopyIcon from '../../icons/copy.svg';
import { useAuth } from '../../hooks/use-auth';
import { useCopyText } from '../../hooks/use-copy-text'; import { useCopyText } from '../../hooks/use-copy-text';
export function EmptyFriends() { type EmptyFriendsProps = {
const user = useAuth(); befriendUrl: string;
};
export function EmptyFriends(props: EmptyFriendsProps) {
const { befriendUrl } = props;
const { isCopied, copyText } = useCopyText(); const { isCopied, copyText } = useCopyText();
const befriendUrl = `https://roadmap.sh/befriend?u=${user?.id}`;
return ( return (
<div class="rounded-md"> <div class="rounded-md">
<div class="flex flex-col items-center p-7 text-center max-w-[450px] mx-auto"> <div class="mx-auto flex flex-col items-center p-7 text-center">
<img <img
alt="no friends" alt="no friends"
src={UserPlusIcon} src={UserPlusIcon}
class="mb-2 h-[60px] w-[60px] opacity-10 sm:h-[120px] sm:w-[120px]" class="mb-2 h-[60px] w-[60px] opacity-10 sm:h-[120px] sm:w-[120px]"
/> />
<h2 class="text-lg font-bold sm:text-xl">Invite your Friends</h2> <h2 class="text-lg font-bold sm:text-xl">Invite your Friends</h2>
<p className="mb-4 mt-1 max-w-[400px] text-sm text-gray-500"> <p className="mb-4 mt-1 max-w-[400px] text-sm leading-loose text-gray-500">
Share the link below with your friends to invite them Invite your friends to join you on your learning journey.
</p> </p>
<div class="flex w-full items-center justify-center gap-2 rounded-lg border-2 p-1 text-sm"> <div class="flex w-full max-w-[352px] items-center justify-center gap-2 rounded-lg border-2 p-1 text-sm">
<input <input
onClick={(e) => { onClick={(e) => {
e.currentTarget.select(); e.currentTarget.select();
@ -33,7 +35,11 @@ export function EmptyFriends() {
readonly readonly
/> />
<button <button
class={`flex items-center justify-center gap-1 rounded-md border-0 p-2 px-3 text-sm text-black ${isCopied ? 'bg-green-300 hover:bg-green-300' : 'bg-gray-200 hover:bg-gray-300'}`} class={`flex items-center justify-center gap-1 rounded-md border-0 p-2 px-3 text-sm text-black ${
isCopied
? 'bg-green-300 hover:bg-green-300'
: 'bg-gray-200 hover:bg-gray-300'
}`}
onClick={() => { onClick={() => {
copyText(befriendUrl); copyText(befriendUrl);
}} }}

@ -1,23 +1,22 @@
import { useEffect, useState } from 'preact/hooks'; import { useEffect } from 'preact/hooks';
import CopyIcon from '../../icons/copy.svg';
import UserPlus from '../../icons/user-plus.svg'; import UserPlus from '../../icons/user-plus.svg';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import { useAuth } from '../../hooks/use-auth'; import { useAuth } from '../../hooks/use-auth';
import {EmptyFriends} from "./EmptyFriends"; import { EmptyFriends } from './EmptyFriends';
export function FriendsPage() { export function FriendsPage() {
const user = useAuth();
useEffect(() => { useEffect(() => {
pageProgressMessage.set(''); pageProgressMessage.set('');
}, []); }, []);
const user = useAuth();
const baseUrl = import.meta.env.DEV const baseUrl = import.meta.env.DEV
? 'http://localhost:3000' ? 'http://localhost:3000'
: 'https://roadmap.sh'; : 'https://roadmap.sh';
const befriendUrl = `${baseUrl}/befriend?u=${user?.id}`; const befriendUrl = `${baseUrl}/befriend?u=${user?.id}`;
return <EmptyFriends /> 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">

@ -0,0 +1,27 @@
type CheckIconProps = {
additionalClasses?: string;
};
export function DeleteUserIcon(props: CheckIconProps) {
const { additionalClasses = 'mr-2 w-[20px] h-[20px]' } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
className={`relative ${additionalClasses}`}
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<line x1="17" x2="22" y1="8" y2="13" />
<line x1="22" x2="17" y1="8" y2="13" />
</svg>
);
}

@ -12,6 +12,7 @@ export interface Props {}
const messageCodes: Record<string, string> = { const messageCodes: Record<string, string> = {
tl: 'Successfully left the team', tl: 'Successfully left the team',
fs: 'Friend request sent',
}; };
export function Toaster(props: Props) { export function Toaster(props: Props) {

@ -0,0 +1,14 @@
---
import AccountLayout from '../layouts/AccountLayout.astro';
import { Befriend } from '../components/Befriend';
import LoginPopup from "../components/AuthenticationFlow/LoginPopup.astro";
---
<AccountLayout
title='Respond Invite'
noIndex={true}
initialLoadingMessage={'Loading invite'}
>
<LoginPopup />
<Befriend client:only />
</AccountLayout>
Loading…
Cancel
Save