mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-19 23:53:24 +02:00
Add friend page
This commit is contained in:
@@ -91,7 +91,7 @@ export function GitHubButton(props: GitHubButtonProps) {
|
||||
// the user was on before they clicked the social login button
|
||||
if (!['/login', '/signup'].includes(window.location.pathname)) {
|
||||
const pagePath =
|
||||
window.location.pathname === '/respond-invite'
|
||||
['/respond-invite', '/befriend'].includes(window.location.pathname)
|
||||
? window.location.pathname + window.location.search
|
||||
: window.location.pathname;
|
||||
|
||||
|
@@ -86,7 +86,7 @@ export function GoogleButton(props: GoogleButtonProps) {
|
||||
// the user was on before they clicked the social login button
|
||||
if (!['/login', '/signup'].includes(window.location.pathname)) {
|
||||
const pagePath =
|
||||
window.location.pathname === '/respond-invite'
|
||||
['/respond-invite', '/befriend'].includes(window.location.pathname)
|
||||
? window.location.pathname + window.location.search
|
||||
: window.location.pathname;
|
||||
|
||||
|
@@ -86,7 +86,7 @@ export function LinkedInButton(props: LinkedInButtonProps) {
|
||||
// the user was on before they clicked the social login button
|
||||
if (!['/login', '/signup'].includes(window.location.pathname)) {
|
||||
const pagePath =
|
||||
window.location.pathname === '/respond-invite'
|
||||
['/respond-invite', '/befriend'].includes(window.location.pathname)
|
||||
? window.location.pathname + window.location.search
|
||||
: window.location.pathname;
|
||||
|
||||
|
226
src/components/Befriend.tsx
Normal file
226
src/components/Befriend.tsx
Normal file
@@ -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 CopyIcon from '../../icons/copy.svg';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import { useCopyText } from '../../hooks/use-copy-text';
|
||||
|
||||
export function EmptyFriends() {
|
||||
const user = useAuth();
|
||||
type EmptyFriendsProps = {
|
||||
befriendUrl: string;
|
||||
};
|
||||
|
||||
export function EmptyFriends(props: EmptyFriendsProps) {
|
||||
const { befriendUrl } = props;
|
||||
const { isCopied, copyText } = useCopyText();
|
||||
const befriendUrl = `https://roadmap.sh/befriend?u=${user?.id}`;
|
||||
|
||||
return (
|
||||
<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
|
||||
alt="no friends"
|
||||
src={UserPlusIcon}
|
||||
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>
|
||||
<p className="mb-4 mt-1 max-w-[400px] text-sm text-gray-500">
|
||||
Share the link below with your friends to invite them
|
||||
<p className="mb-4 mt-1 max-w-[400px] text-sm leading-loose text-gray-500">
|
||||
Invite your friends to join you on your learning journey.
|
||||
</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
|
||||
onClick={(e) => {
|
||||
e.currentTarget.select();
|
||||
@@ -33,7 +35,11 @@ export function EmptyFriends() {
|
||||
readonly
|
||||
/>
|
||||
<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={() => {
|
||||
copyText(befriendUrl);
|
||||
}}
|
||||
|
@@ -1,23 +1,22 @@
|
||||
import { useEffect, useState } from 'preact/hooks';
|
||||
import CopyIcon from '../../icons/copy.svg';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import UserPlus from '../../icons/user-plus.svg';
|
||||
import { pageProgressMessage } from '../../stores/page';
|
||||
import { useAuth } from '../../hooks/use-auth';
|
||||
import {EmptyFriends} from "./EmptyFriends";
|
||||
import { EmptyFriends } from './EmptyFriends';
|
||||
|
||||
export function FriendsPage() {
|
||||
const user = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
pageProgressMessage.set('');
|
||||
}, []);
|
||||
|
||||
const user = useAuth();
|
||||
const baseUrl = import.meta.env.DEV
|
||||
? 'http://localhost:3000'
|
||||
: 'https://roadmap.sh';
|
||||
const befriendUrl = `${baseUrl}/befriend?u=${user?.id}`;
|
||||
|
||||
return <EmptyFriends />
|
||||
return <EmptyFriends befriendUrl={befriendUrl} />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
|
27
src/components/ReactIcons/DeleteUserIcon.tsx
Normal file
27
src/components/ReactIcons/DeleteUserIcon.tsx
Normal file
@@ -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> = {
|
||||
tl: 'Successfully left the team',
|
||||
fs: 'Friend request sent',
|
||||
};
|
||||
|
||||
export function Toaster(props: Props) {
|
||||
|
14
src/pages/befriend.astro
Normal file
14
src/pages/befriend.astro
Normal file
@@ -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>
|
Reference in New Issue
Block a user