Friends and notification pages

images-fix
Kamran Ahmed 1 year ago
parent 2f07d5edf0
commit b47f712dec
  1. 19
      src/components/Friends/FriendsPage.tsx
  2. 4
      src/components/Friends/InviteFriendPopup.tsx
  3. 66
      src/components/Notification/NotificationPage.tsx
  4. 24
      src/components/ReactIcons/AcceptIcon.tsx
  5. 10
      src/components/TeamProgress/GroupRoadmapItem.tsx

@ -7,10 +7,10 @@ import type { FriendshipStatus } from '../Befriend';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { EmptyFriends } from './EmptyFriends'; import { EmptyFriends } from './EmptyFriends';
import { FriendProgressItem } from './FriendProgressItem'; import { FriendProgressItem } from './FriendProgressItem';
import UserIcon from '../../icons/user.svg';
import { UserProgressModal } from '../UserProgress/UserProgressModal'; import { UserProgressModal } from '../UserProgress/UserProgressModal';
import { InviteFriendPopup } from './InviteFriendPopup'; import { InviteFriendPopup } from './InviteFriendPopup';
import { UserCustomProgressModal } from '../UserProgress/UserCustomProgressModal'; import { UserCustomProgressModal } from '../UserProgress/UserCustomProgressModal';
import { UserIcon } from '../ReactIcons/UserIcon.tsx';
type FriendResourceProgress = { type FriendResourceProgress = {
updatedAt: string; updatedAt: string;
@ -64,7 +64,7 @@ export function FriendsPage() {
async function loadFriends() { async function loadFriends() {
const { response, error } = await httpGet<ListFriendsResponse>( const { response, error } = await httpGet<ListFriendsResponse>(
`${import.meta.env.PUBLIC_API_URL}/v1-list-friends` `${import.meta.env.PUBLIC_API_URL}/v1-list-friends`,
); );
if (error || !response) { if (error || !response) {
@ -89,15 +89,15 @@ export function FriendsPage() {
const befriendUrl = `${baseUrl}/befriend?u=${user?.id}`; const befriendUrl = `${baseUrl}/befriend?u=${user?.id}`;
const selectedGroupingType = groupingTypes.find( const selectedGroupingType = groupingTypes.find(
(grouping) => grouping.value === selectedGrouping (grouping) => grouping.value === selectedGrouping,
); );
const filteredFriends = friends.filter((friend) => const filteredFriends = friends.filter(
selectedGroupingType?.statuses.includes(friend.status) (friend) => selectedGroupingType?.statuses.includes(friend.status),
); );
const receivedRequests = friends.filter( const receivedRequests = friends.filter(
(friend) => friend.status === 'received' (friend) => friend.status === 'received',
); );
if (isLoading) { if (isLoading) {
@ -203,11 +203,8 @@ export function FriendsPage() {
{filteredFriends.length === 0 && ( {filteredFriends.length === 0 && (
<div className="flex flex-col items-center justify-center py-12"> <div className="flex flex-col items-center justify-center py-12">
<img <UserIcon className="mb-3 w-12 opacity-20" />
src={UserIcon.src}
alt="Empty Friends"
className="mb-3 w-12 opacity-20"
/>
<h2 className="text-lg font-semibold"> <h2 className="text-lg font-semibold">
{selectedGrouping === 'active' && 'No friends yet'} {selectedGrouping === 'active' && 'No friends yet'}
{selectedGrouping === 'sent' && 'No requests sent'} {selectedGrouping === 'sent' && 'No requests sent'}

@ -2,7 +2,7 @@ import type { MouseEvent } from 'react';
import { useRef } from 'react'; import { useRef } from 'react';
import { useOutsideClick } from '../../hooks/use-outside-click'; import { useOutsideClick } from '../../hooks/use-outside-click';
import { useCopyText } from '../../hooks/use-copy-text'; import { useCopyText } from '../../hooks/use-copy-text';
import { Copy } from 'lucide-react'; import { CopyIcon } from 'lucide-react';
type InviteFriendPopupProps = { type InviteFriendPopupProps = {
befriendUrl: string; befriendUrl: string;
@ -54,7 +54,7 @@ export function InviteFriendPopup(props: InviteFriendPopupProps) {
copyText(befriendUrl); copyText(befriendUrl);
}} }}
> >
<Copy className="h-4 w-4" /> <CopyIcon className="mr-1 h-4 w-4" />
{isCopied ? 'Copied' : 'Copy URL'} {isCopied ? 'Copied' : 'Copy URL'}
</button> </button>
</div> </div>

@ -1,10 +1,10 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { httpGet, httpPatch, httpPost } from '../../lib/http'; import { httpGet, httpPatch } from '../../lib/http';
import { pageProgressMessage } from '../../stores/page'; import { pageProgressMessage } from '../../stores/page';
import type { TeamMemberDocument } from '../TeamMembers/TeamMembersPage'; import type { TeamMemberDocument } from '../TeamMembers/TeamMembersPage';
import XIcon from '../../icons/close-dark.svg'; import XIcon from '../../icons/close-dark.svg';
import AcceptIcon from '../../icons/accept.svg';
import { useToast } from '../../hooks/use-toast'; import { useToast } from '../../hooks/use-toast';
import { AcceptIcon } from '../ReactIcons/AcceptIcon.tsx';
interface NotificationList extends TeamMemberDocument { interface NotificationList extends TeamMemberDocument {
name: string; name: string;
@ -18,7 +18,7 @@ export function NotificationPage() {
const lostNotifications = async () => { const lostNotifications = async () => {
const { error, response } = await httpGet<NotificationList[]>( const { error, response } = await httpGet<NotificationList[]>(
`${import.meta.env.PUBLIC_API_URL}/v1-get-invitation-list` `${import.meta.env.PUBLIC_API_URL}/v1-get-invitation-list`,
); );
if (error || !response) { if (error || !response) {
toast.error(error?.message || 'Something went wrong'); toast.error(error?.message || 'Something went wrong');
@ -28,28 +28,37 @@ export function NotificationPage() {
setNotifications(response); setNotifications(response);
}; };
async function respondInvitation(status: 'accept' | 'reject', inviteId: string) { async function respondInvitation(
status: 'accept' | 'reject',
inviteId: string,
) {
setIsLoading(true); setIsLoading(true);
setError(''); setError('');
const { response, error } = await httpPatch<{ teamId: string }>( const { response, error } = await httpPatch<{ teamId: string }>(
`${import.meta.env.PUBLIC_API_URL}/v1-respond-invite/${inviteId}`, { `${import.meta.env.PUBLIC_API_URL}/v1-respond-invite/${inviteId}`,
status {
}); status,
},
);
if (error || !response) { if (error || !response) {
setError(error?.message || 'Something went wrong') setError(error?.message || 'Something went wrong');
setIsLoading(false) setIsLoading(false);
return; return;
} }
if (status === 'accept') { if (status === 'accept') {
window.location.href = `/team/progress?t=${response.teamId}`; window.location.href = `/team/progress?t=${response.teamId}`;
} else { } else {
window.dispatchEvent(new CustomEvent('refresh-notification', { window.dispatchEvent(
new CustomEvent('refresh-notification', {
detail: { detail: {
count: notifications.length - 1 count: notifications.length - 1,
} },
})); }),
setNotifications(notifications.filter((notification) => notification._id !== inviteId)); );
setNotifications(
notifications.filter((notification) => notification._id !== inviteId),
);
setIsLoading(false); setIsLoading(false);
} }
} }
@ -66,15 +75,20 @@ export function NotificationPage() {
<h2 className="text-3xl font-bold sm:text-4xl">Notification</h2> <h2 className="text-3xl font-bold sm:text-4xl">Notification</h2>
<p className="mt-2 text-gray-400">Manage your notifications</p> <p className="mt-2 text-gray-400">Manage your notifications</p>
</div> </div>
{ {notifications.length === 0 && (
notifications.length === 0 && ( <div className="mt-6 flex items-center justify-center">
<div className="flex items-center justify-center mt-6">
<p className="text-gray-400"> <p className="text-gray-400">
No notifications, you can <a href="/team/new" className="text-blue-500 underline hover:no-underline">create a team</a> and invite your friends to join. No notifications, you can{' '}
<a
href="/team/new"
className="text-blue-500 underline hover:no-underline"
>
create a team
</a>{' '}
and invite your friends to join.
</p> </p>
</div> </div>
) )}
}
<div className="space-y-4"> <div className="space-y-4">
{notifications.map((notification) => ( {notifications.map((notification) => (
<div className="flex items-center justify-between rounded-md border p-2"> <div className="flex items-center justify-between rounded-md border p-2">
@ -86,16 +100,18 @@ export function NotificationPage() {
</div> </div>
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<button type="button" <button
type="button"
disabled={isLoading} disabled={isLoading}
className="inline-flex border p-1 rounded hover:bg-gray-50 disabled:opacity-75" className="inline-flex rounded border p-1 hover:bg-gray-50 disabled:opacity-75"
onClick={() => respondInvitation('accept', notification?._id!)} onClick={() => respondInvitation('accept', notification?._id!)}
> >
<img src={AcceptIcon.src} className="h-4 w-4" /> <AcceptIcon className="h-4 w-4" />
</button> </button>
<button type="button" <button
type="button"
disabled={isLoading} disabled={isLoading}
className="inline-flex border p-1 rounded hover:bg-gray-50 disabled:opacity-75" className="inline-flex rounded border p-1 hover:bg-gray-50 disabled:opacity-75"
onClick={() => respondInvitation('reject', notification?._id!)} onClick={() => respondInvitation('reject', notification?._id!)}
> >
<img alt={'Close'} src={XIcon.src} className="h-4 w-4" /> <img alt={'Close'} src={XIcon.src} className="h-4 w-4" />

@ -0,0 +1,24 @@
type AcceptIconProps = {
className?: string;
};
export function AcceptIcon(props: AcceptIconProps) {
const { className } = props;
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="#000"
className={className}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
);
}

@ -1,8 +1,8 @@
import { useState } from 'react'; import { useState } from 'react';
import type { GroupByRoadmap, TeamMember } from './TeamProgressPage'; import type { GroupByRoadmap, TeamMember } from './TeamProgressPage';
import { getUrlParams } from '../../lib/browser'; import { getUrlParams } from '../../lib/browser';
import ExternalLinkIcon from '../../icons/external-link.svg';
import { useAuth } from '../../hooks/use-auth'; import { useAuth } from '../../hooks/use-auth';
import { LucideExternalLink } from 'lucide-react';
type GroupRoadmapItemProps = { type GroupRoadmapItemProps = {
roadmap: GroupByRoadmap; roadmap: GroupByRoadmap;
@ -33,11 +33,7 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
className="group mb-0.5 flex shrink-0 items-center justify-between text-base font-medium leading-none text-black" className="group mb-0.5 flex shrink-0 items-center justify-between text-base font-medium leading-none text-black"
target={'_blank'} target={'_blank'}
> >
<img <LucideExternalLink className="h-4 w-4 opacity-20 transition-opacity group-hover:opacity-100" />
alt={'link'}
src={ExternalLinkIcon.src}
className="ml-2 h-4 w-4 opacity-20 transition-opacity group-hover:opacity-100"
/>
</a> </a>
</div> </div>
</div> </div>
@ -58,7 +54,7 @@ export function GroupRoadmapItem(props: GroupRoadmapItemProps) {
onClick={() => { onClick={() => {
onShowResourceProgress( onShowResourceProgress(
member.member, member.member,
member.progress?.resourceId! member.progress?.resourceId!,
); );
}} }}
> >

Loading…
Cancel
Save