Skip to content

Commit

Permalink
Merge pull request #20 from JasonCrk/issue-19-notifications-page
Browse files Browse the repository at this point in the history
Notifications page
  • Loading branch information
JasonCrk authored Nov 20, 2023
2 parents 680c072 + d904a27 commit 191ce9c
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 46 deletions.
1 change: 1 addition & 0 deletions src/components/IconButton.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const IconButton: FC<Props> = ({
padding: '5px 10px 8px 10px',
textAlign: 'center',
fontSize: style?.fontSize ? style.fontSize : '1.1rem',
...style,
}}
{...props}
>
Expand Down
144 changes: 101 additions & 43 deletions src/components/InvitationItem.component.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,111 @@
import { FC } from 'react';
import Avatar from './Avatar.component';
import Button from './Button.component';

interface Props {
userId: string;
avatar: string;
username: string;
notifiedAt: string;
}
import { FC, useState } from 'react'

import { Link } from 'react-router-dom'

import { useMutation } from '@tanstack/react-query'

import { InvitationResponse } from '../models/invitation.model'

import {
acceptInvitation,
rejectInvitation,
} from '../services/invitation.service'

import Avatar from './Avatar.component'
import IconButton from './IconButton.component'

import { datetimeFormat } from '../utils/datetimeFormat'

import { IoMdCheckmark } from 'react-icons/io'
import { AiOutlineClose } from 'react-icons/ai'

const InvitationItem: FC<Props> = ({
userId,
avatar,
username,
const InvitationItem: FC<InvitationResponse> = ({
id: invitationId,
user,
notifiedAt,
}) => {
const handleAccept = () => {
console.log(`Invitación aceptada para el usuario con ID: ${userId}`);
};
const [isAccepted, setIsAccepted] = useState<boolean | null>(null)

const handleReject = () => {
console.log(`Invitación rechazada para el usuario con ID: ${userId}`);
};
const {
isPending: isPendingAcceptInvitation,
mutate: mutateAcceptInvitation,
} = useMutation({
mutationFn: acceptInvitation,
onSuccess: () => setIsAccepted(true),
})

const redirectToProfile = () => {
window.location.href = `/${userId}/profile`;
};
const {
isPending: isPendingRejectInvitation,
mutate: mutateRejectInvitation,
} = useMutation({
mutationFn: rejectInvitation,
onSuccess: () => setIsAccepted(false),
})

const handleAcceptInvitation = () => {
mutateAcceptInvitation(invitationId)
}

const handleRejectInvitation = () => {
mutateRejectInvitation(invitationId)
}

return (
<div className="mx-auto d-flex align-items-center border rounded p-3 bg-white text-dark">
<div onClick={redirectToProfile}>
<Avatar size="50px" src={avatar} alt={`${username}'s Avatar`} />
</div>
<div className="ms-3 flex-grow-1">
<div className="fw-bold">{username} quiere conectarse con usted</div>
<div>{notifiedAt}</div>
</div>
<div className="ms-3"> {/* Añadido espacio entre los botones */}
<Button btnColor="success" onClick={handleAccept}>
Aceptar
</Button>
</div>
<div className="ms-2"> {/* Añadido espacio entre los botones */}
<Button btnColor="danger" onClick={handleReject}>
Rechazar
</Button>
<div
className={`card ${
isAccepted !== null &&
(isAccepted ? 'bg-success-subtle' : 'bg-danger-subtle')
}`}
>
<div className='card-body d-flex align-items-center justify-content-between'>
<div className='d-flex align-items-center gap-2'>
<Link to={`/${user.id}/profile`}>
<Avatar
size='55px'
src={user.account.picture}
alt={user.username}
className='border border-secondary'
/>
</Link>
<div>
<p className='mb-0' style={{ fontSize: '1.05rem' }}>
<Link
to={`/${user.id}/profile`}
className='fw-bold link-offset-1 link-offset-1-hover link-underline link-underline-opacity-0 link-underline-opacity-100-hover text-dark link-secondary'
>
{user.username}
</Link>{' '}
quiere conectarse con usted
</p>
<p className='mb-0' style={{ fontSize: '0.9rem' }}>
{datetimeFormat(notifiedAt, 'short', 'medium')}
</p>
</div>
</div>

{isAccepted === null ? (
<div className='d-flex justify-content-center align-items-center gap-2'>
<IconButton
icon={<IoMdCheckmark />}
className='btn-success'
onClick={handleAcceptInvitation}
disabled={isPendingRejectInvitation || isPendingAcceptInvitation}
/>
<IconButton
icon={<AiOutlineClose />}
className='btn-danger'
onClick={handleRejectInvitation}
disabled={isPendingRejectInvitation || isPendingAcceptInvitation}
/>
</div>
) : isAccepted ? (
<span className='badge bg-success fs-6'>Accepted</span>
) : (
<span className='badge bg-danger fs-6'>Rejected</span>
)}
</div>
</div>
);
};
)
}

export default InvitationItem;
export default InvitationItem
38 changes: 38 additions & 0 deletions src/components/skeleton/InvitationItemSkeleton.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { FC } from 'react'

const InvitationItemSkeleton: FC = () => {
return (
<div className='card'>
<div className='card-body d-flex align-items-center justify-content-between'>
<div className='d-flex align-items-center gap-2'>
<div
className='bg-secondary rounded-circle'
style={{ width: '55px', height: '55px' }}
></div>
<p className='placeholder-glow mb-0 d-flex flex-column gap-1'>
<span
className='placeholder placeholder-lg'
style={{ width: '300px' }}
></span>
<span
className='placeholder placeholder-lg'
style={{ width: '150px' }}
></span>
</p>
</div>
<div className='d-flex justify-content-center align-items-center gap-2'>
<button
className='btn btn-success disabled placeholder rounded'
style={{ width: '50px', height: '50px' }}
></button>
<button
className='btn btn-danger disabled placeholder rounded'
style={{ width: '50px', height: '50px' }}
></button>
</div>
</div>
</div>
)
}

export default InvitationItemSkeleton
11 changes: 10 additions & 1 deletion src/models/invitation.model.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { User } from './user.model'
import { SimpleUser, User } from './user.model'

export type InvitationId = `${string}-${string}-${string}-${string}-${string}`
export type InvitationInviter = User
export type InvitationInviting = User
export type InvitationNotifiedAt = string

export enum InvitationStatus {
ACCEPTED = 'ACCEPTED',
Expand All @@ -13,4 +14,12 @@ export interface Invitation {
id: InvitationId
inviter: InvitationInviter
inviting: InvitationInviting
notifiedAt: InvitationNotifiedAt
status: InvitationStatus
}

export interface InvitationResponse {
id: InvitationId
user: SimpleUser
notifiedAt: InvitationNotifiedAt
}
31 changes: 31 additions & 0 deletions src/pages/Notifications.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useQuery } from '@tanstack/react-query'

import { getAllInvitations } from '../services/invitation.service'

import InvitationItem from '../components/InvitationItem.component'
import InvitationItemSkeleton from '../components/skeleton/InvitationItemSkeleton.component'

function Notifications() {
const { isLoading, data: invitations } = useQuery({
queryKey: ['notifications'],
queryFn: () => getAllInvitations(),
})

return (
<div className='d-flex flex-column mt-4'>
{isLoading || !invitations ? (
[...Array(6)].map(() => (
<InvitationItemSkeleton key={crypto.randomUUID()} />
))
) : invitations.data.length > 0 ? (
invitations.data.map(invitation => (
<InvitationItem {...invitation} key={invitation.id} />
))
) : (
<h4 className='text-center'>No tienes ninguna notificación</h4>
)}
</div>
)
}

export default Notifications
5 changes: 5 additions & 0 deletions src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import CreateEvent from './pages/CreateEvent.page'
import EventDetails from './pages/EventDetails.page'
import UserProfile from './pages/UserProfile.page'
import Network from './pages/Network.page'
import Notifications from './pages/Notifications.page'

import { isAuthenticatedLoader, isNotAuthenticatedLoader } from './loaders'

Expand Down Expand Up @@ -47,6 +48,10 @@ const router = createBrowserRouter([
path: '/networks',
element: <Network />,
},
{
path: '/notifications',
element: <Notifications />,
},
{
path: '/events/:eventId',
element: <EventDetails />,
Expand Down
30 changes: 28 additions & 2 deletions src/services/invitation.service.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,24 @@
import { UserId } from '../models/user.model'
import { MessageResponse } from '../models/response.model'
import { InvitationId } from '../models/invitation.model'
import { ListResponse, MessageResponse } from '../models/response.model'
import { InvitationId, InvitationResponse } from '../models/invitation.model'

import { useAuthStore } from '../store/useAuthStorage'

import { connectionBaseEndpoint } from './endpoints'

export const getAllInvitations = async (): Promise<
ListResponse<InvitationResponse>
> => {
const accessToken = useAuthStore.getState().accessToken
return connectionBaseEndpoint
.get<ListResponse<InvitationResponse>>('/invitations', {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
.then(response => response.data)
}

export const sendInvitation = async (
userId: UserId
): Promise<MessageResponse> => {
Expand Down Expand Up @@ -35,3 +48,16 @@ export const acceptInvitation = async (
})
.then(response => response.data)
}

export const rejectInvitation = async (
invitationId: InvitationId
): Promise<MessageResponse> => {
const accessToken = useAuthStore.getState().accessToken
return connectionBaseEndpoint
.delete<MessageResponse>(`/reject-invitation/${invitationId}`, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
.then(response => response.data)
}

0 comments on commit 191ce9c

Please sign in to comment.