Skip to content

Commit

Permalink
Contacts page frontend integration with backend (#167)
Browse files Browse the repository at this point in the history
* Contacts page frontend integration with backend

Co-authored-by: Mahid Ahmad <[email protected]>
* Refactor contacts API and improve ContactsTable component for better data handling and presentation
  • Loading branch information
SajanGhuman authored Jan 20, 2025
1 parent d0fdae3 commit 69cd27b
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 74 deletions.
102 changes: 66 additions & 36 deletions Client/src/app/api/contacts/route.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,73 @@
import bcryptjs from 'bcryptjs';
import prisma from '@lib/prisma';
import { NextResponse, NextRequest } from 'next/server';
import { NextRequest, NextResponse } from 'next/server';
import { authenticate } from '@lib/middleware/authenticate';

export async function GET(req: NextRequest): Promise<NextResponse> {
try {
const userId = await authenticate(req);

const visitors = await prisma.linkVisitors.groupBy({
by: ['email'],
_count: {
email: true,
},
_max: {
updatedAt: true,
},
});

const visitorDetails = await Promise.all(visitors.map(async (visitor) => {
const lastVisit = await prisma.linkVisitors.findFirst({
where: { email: visitor.email },
orderBy: { updatedAt: 'desc' },
include: { Link: true },
});

return {
firstName: lastVisit?.first_name || 'N/A',
lastName: lastVisit?.last_name || 'N/A',
email: visitor.email || 'N/A',
lastViewedLink: lastVisit?.Link?.linkUrl || lastVisit?.Link.friendlyName || 'N/A',
lastActivity: lastVisit?.updatedAt || 'N/A',
totalVisits: visitor._count.email,
};
}));

return NextResponse.json({ data: visitorDetails }, { status: 200 });
} catch (error) {
return createErrorResponse('Server error.', 500, error);
}
try {
const userId = await authenticate(req);

const userLinks = await prisma.link.findMany({
where: { userId },
select: { linkId: true },
});
if (!userLinks.length) {
return NextResponse.json({ data: [] }, { status: 200 });
}

const linkIds = userLinks.map((l) => l.linkId);

const visitors = await prisma.linkVisitors.groupBy({
by: ['email'],
where: {
linkId: { in: linkIds },
},
_count: {
email: true,
},
_max: {
updatedAt: true,
},
});

const visitorDetails = await Promise.all(
visitors.map(async (visitor) => {
const lastVisit = await prisma.linkVisitors.findFirst({
where: {
email: visitor.email,
linkId: { in: linkIds },
},
orderBy: { updatedAt: 'desc' },
include: {
Link: true,
},
});

if (!lastVisit) {
return null;
}

const firstName = lastVisit.first_name?.trim() || null;
const lastName = lastVisit.last_name?.trim() || null;
const fullName =
firstName || lastName ? `${firstName || ''} ${lastName || ''}`.trim() : null;

return {
id: lastVisit.id,
name: fullName,
email: visitor.email || null,
lastViewedLink: lastVisit.Link?.friendlyName || lastVisit.Link?.linkUrl || null,
lastActivity: lastVisit.updatedAt || null,
totalVisits: visitor._count.email || 0,
};
}),
);

const contacts = visitorDetails.filter(Boolean);

return NextResponse.json({ data: contacts }, { status: 200 });
} catch (error) {
return createErrorResponse('Server error.', 500, error);
}
}

function createErrorResponse(message: string, status: number, details?: any) {
Expand Down
91 changes: 68 additions & 23 deletions Client/src/app/contacts/components/ContactsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use client';
import Paginator from '@/components/Paginator';
import { dummyData } from '@/data/dummyContacts';
import { useSort } from '@/hooks/useSort';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';

import React, { useEffect, useState } from 'react';
import axios from 'axios';

import {
Box,
CircularProgress,
Paper,
Table,
TableBody,
Expand All @@ -13,31 +14,76 @@ import {
TableHead,
TableRow,
TableSortLabel,
Typography,
} from '@mui/material';
import { useState } from 'react';
import ContactsTableRow from './ContactsTableRow';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import UnfoldMoreIcon from '@mui/icons-material/UnfoldMore';

import { Contact } from '@/utils/shared/models';
import { useSort } from '@/hooks/useSort';
import Paginator from '@/components/Paginator';
import ContactsTableRow from './ContactsTableRow';

const ContactsTable = () => {
export default function ContactsTable() {
const pageSize = 12;
const [page, setPage] = useState(1);
const [data, setData] = useState<Contact[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
fetchContacts();
}, []);

const fetchContacts = async () => {
try {
const response = await axios.get('/api/contacts');
const contacts = response.data.data as Contact[];

const parsedContacts = contacts.map((contact) => ({
...contact,
lastActivity: new Date(contact.lastActivity),
}));
setData(parsedContacts);
} catch (err: any) {
setError(err.message || 'An error occurred while fetching contacts.');
} finally {
setLoading(false);
}
};

// Sort the entire data set
const { sortedData, orderDirection, orderBy, handleSortRequest } = useSort<Contact>(
dummyData,
undefined,
(a: Contact, b: Contact, orderDirection: 'asc' | 'desc' | undefined): number => {
const timeA = new Date(a.lastActivity).getTime();
const timeB = new Date(b.lastActivity).getTime();
return orderDirection === 'asc' ? timeA - timeB : timeB - timeA;
},
data,
'lastActivity',
);

// Paginate the sorted data
const paginatedData = sortedData.slice((page - 1) * pageSize, page * pageSize);

const totalPages = Math.ceil(sortedData.length / pageSize);

if (loading) {
return (
<Box
display='flex'
justifyContent='center'
mt={4}>
<CircularProgress />
</Box>
);
}

if (error) {
return (
<Box mt={4}>
<Typography
color='error'
align='center'
variant='h6'>
{error}
</Typography>
</Box>
);
}

return (
<>
<TableContainer component={Paper}>
Expand All @@ -64,24 +110,23 @@ const ContactsTable = () => {
<TableBody>
{paginatedData.map((row) => (
<ContactsTableRow
key={row.userId}
key={row.id}
contact={row}
/>
))}
</TableBody>
</Table>
</TableContainer>

{totalPages > 1 && (
<Paginator
page={page}
totalPages={totalPages}
onPageChange={setPage}
pageSize={pageSize}
totalItems={dummyData.length}
totalItems={data.length}
/>
)}
</>
);
};

export default ContactsTable;
}
31 changes: 16 additions & 15 deletions Client/src/app/contacts/components/ContactsTableRow.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { TableCell, TableRow, Typography } from '@mui/material';

import { Contact } from '@/utils/shared/models';
import { formatDateTime } from '@/utils/shared/utils';
import { TableCell, TableRow, Typography } from '@mui/material';

interface Props {
contact: Contact;
}

const ContactsTableRow = ({ contact }: Props) => (
<TableRow>
<TableCell>
{contact.name}
<br />
<Typography variant='caption'>{contact.email}</Typography>
</TableCell>
<TableCell>{contact.lastViewedLink}</TableCell>
<TableCell>{formatDateTime(contact.lastActivity, { includeTime: true })}</TableCell>
<TableCell>{contact.visits}</TableCell>
</TableRow>
);

export default ContactsTableRow;
export default function ContactsTableRow({ contact }: Props) {
return (
<TableRow>
<TableCell>
{contact.name ? contact.name : 'N/A'}
<br />
<Typography variant='caption'>{contact.email ? contact.email : 'N/A'}</Typography>
</TableCell>
<TableCell>{contact.lastViewedLink}</TableCell>
<TableCell>{formatDateTime(contact.lastActivity, { includeTime: true })}</TableCell>
<TableCell>{contact.totalVisits}</TableCell>
</TableRow>
);
}

0 comments on commit 69cd27b

Please sign in to comment.