mirror of
https://github.com/kamranahmedse/developer-roadmap.git
synced 2025-08-20 08:02:35 +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
|
// 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;
|
||||||
|
|
||||||
|
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 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">
|
||||||
|
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> = {
|
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) {
|
||||||
|
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