Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notifications page #20

Merged
merged 1 commit into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
}