Skip to content

Commit

Permalink
better offerlist UX
Browse files Browse the repository at this point in the history
  • Loading branch information
januschung committed Dec 20, 2024
1 parent 6536cc7 commit 0a0b6c7
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 64 deletions.
1 change: 1 addition & 0 deletions src/components/AppHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ export default function AppHeader() {
handleMobileMenuClose={handleMobileMenuClose}
handleProfileMenuOpen={handleProfileMenuOpen}
handleJobApplicationOpen={handleOpen}
handleOfferListDialogOpen={handleOfferListDialogOpen}
interviewCount={interviewCount}
offerCount={offerCount}
/>
Expand Down
4 changes: 2 additions & 2 deletions src/components/MobileMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import EventAvailableIcon from '@mui/icons-material/EventAvailable';
import NotificationsIcon from '@mui/icons-material/Notifications';
import AccountCircle from '@mui/icons-material/AccountCircle';

export default function MobileMenu({ mobileMoreAnchorEl, isMobileMenuOpen, handleMobileMenuClose, handleProfileMenuOpen, handleJobApplicationOpen, interviewCount, offerCount }) {
export default function MobileMenu({ mobileMoreAnchorEl, isMobileMenuOpen, handleMobileMenuClose, handleProfileMenuOpen, handleJobApplicationOpen, handleOfferListDialogOpen, interviewCount, offerCount }) {
const mobileMenuId = 'primary-search-account-menu-mobile';
return (
<Menu
Expand Down Expand Up @@ -40,7 +40,7 @@ export default function MobileMenu({ mobileMoreAnchorEl, isMobileMenuOpen, handl
</IconButton>
<p>Interviews</p>
</MenuItem>
<MenuItem>
<MenuItem onClick={handleOfferListDialogOpen}>
<IconButton size="large" color="inherit">
<Badge badgeContent={offerCount} color="error">
<NotificationsIcon />
Expand Down
104 changes: 42 additions & 62 deletions src/components/OfferListDialog.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,22 @@ import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import CircularProgress from '@mui/material/CircularProgress';
import Box from '@mui/material/Box';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper';
import { GET_ALL_OFFERS } from '../graphql/query';
import useJobApplicationDialog from './hooks/useJobApplicationDialog';
import JobApplicationDialog from './JobApplicationDialog';
import DialogTitleBar from './DialogTitleBar';
import useSortableTable from './hooks/useSortableTable';
import SortableTable from './common/SortableTable';

export default function OfferListDialog({ handleClose, open }) {
const [offers, setOffers] = useState([]);

const { loading, error, data, refetch } = useQuery(GET_ALL_OFFERS, {
fetchPolicy: 'network-only',
onCompleted: (data) => setOffers(data.allOffer),
});

const {
open: jobDialogOpen,
jobApplication,
Expand All @@ -35,6 +32,35 @@ export default function OfferListDialog({ handleClose, open }) {
refetch().then(({ data }) => setOffers(data.allOffer));
});

const columns = [
{
key: 'jobApplication.companyName', // Nested property example
label: 'Company Name',
sortable: true,
render: (value, row) => value || row.jobApplication?.companyName || 'N/A', // Fallback for nested value
},
{ key: 'offerDate', label: 'Offer Date', sortable: true },
{ key: 'salaryOffered', label: 'Salary Offered', sortable: true },
{ key: 'description', label: 'Note' },
{
key: 'actions',
label: 'Job Application',
render: (value, row) => (
<Button
size="small"
color="info"
variant="outlined"
onClick={() => handleJobDialogOpen(row.jobApplication)}
>
Job Details
</Button>
),
align: 'center',
},
];

const { sortedData, handleSort, getSortIndicator } = useSortableTable(offers, columns);

useEffect(() => {
if (open) {
refetch()
Expand All @@ -49,8 +75,6 @@ export default function OfferListDialog({ handleClose, open }) {
<Dialog
open={open}
onClose={handleClose}
aria-labelledby="modal-title"
aria-describedby="modal-description"
fullWidth
maxWidth="md"
slotProps={{
Expand All @@ -62,68 +86,24 @@ export default function OfferListDialog({ handleClose, open }) {
}}
>
<DialogTitleBar title="Offer List" />

<DialogContent dividers>
{loading && (
{loading ? (
<Box display="flex" justifyContent="center" alignItems="center" height="200px">
<CircularProgress />
</Box>
)}

{error && (
) : error ? (
<Typography variant="body1" color="error" align="center">
Something went wrong. Please try again later.
</Typography>
)}

{!loading && !error && offers.length === 0 && (
<Box textAlign="center" p={4}>
<Typography variant="h6" color="textSecondary">
No offers available yet!
</Typography>
<Button color="primary" variant="contained" onClick={handleClose}>
Add an Offer
</Button>
</Box>
)}

{!loading && !error && offers.length > 0 && (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell align="left">Company</TableCell>
<TableCell align="left">Offer Date</TableCell>
<TableCell align="left">Salary Offered</TableCell>
<TableCell align="left">Note</TableCell>
<TableCell align="center">Job Application</TableCell>
</TableRow>
</TableHead>
<TableBody>
{offers.map((offer, index) => (
<TableRow key={index}>
<TableCell align="left">{offer.jobApplication.companyName}</TableCell>
<TableCell align="left">{offer.offerDate}</TableCell>
<TableCell align="left">{offer.salaryOffered}</TableCell>
<TableCell align="left">{offer.description}</TableCell>
<TableCell align="center">
<Button
size="small"
color="info"
variant="outlined"
onClick={() => handleJobDialogOpen(offer.jobApplication)}
>
Job Details
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
) : (
<SortableTable
data={sortedData}
columns={columns}
handleSort={handleSort}
getSortIndicator={getSortIndicator}
/>
)}
</DialogContent>

<DialogActions>
<Button color="info" variant="outlined" startIcon={<CancelIcon />} onClick={handleClose}>
Cancel
Expand Down
58 changes: 58 additions & 0 deletions src/components/common/SortableTable.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import TableSortLabel from '@mui/material/TableSortLabel';
import Paper from '@mui/material/Paper';

const SortableTable = ({ data, columns, handleSort, getSortIndicator }) => {
return (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
{columns.map((column) => (
<TableCell
key={column.key}
align={column.align || 'left'}
style={{
cursor: 'pointer',
fontWeight: 'bold',
backgroundColor: '#e3f2fd',
}}
>
{column.sortable ? (
<TableSortLabel
active={getSortIndicator(column.key)}
direction={getSortIndicator(column.key) || 'asc'}
onClick={() => handleSort(column.key)}
>
{column.label}
</TableSortLabel>
) : (
column.label
)}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{data.map((row, index) => (
<TableRow key={index}>
{columns.map((column) => (
<TableCell key={column.key} align={column.align || 'left'}>
{column.render ? column.render(row[column.key], row) : row[column.key]}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};

export default SortableTable;
40 changes: 40 additions & 0 deletions src/components/hooks/useSortableTable.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useState } from 'react';

const useSortableTable = (data, columns) => {
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });

const handleSort = (key) => {
const direction = sortConfig.key === key && sortConfig.direction === 'asc' ? 'desc' : 'asc';
setSortConfig({ key, direction });
};

// Function to get the value from a nested object
const getValue = (item, key) => {
const keys = key.split('.'); // Split key into parts for nested access
return keys.reduce((obj, keyPart) => obj && obj[keyPart], item);
};

const sortedData = sortConfig.key
? [...data].sort((a, b) => {
const aValue = getValue(a, sortConfig.key);
const bValue = getValue(b, sortConfig.key);

if (aValue < bValue) return sortConfig.direction === 'asc' ? -1 : 1;
if (aValue > bValue) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
})
: data;

const getSortIndicator = (key) => {
if (sortConfig.key !== key) return '';
return sortConfig.direction === 'asc' ? '↑' : '↓';
};

return {
sortedData,
handleSort,
getSortIndicator,
};
};

export default useSortableTable;
18 changes: 18 additions & 0 deletions src/util/sortUtil.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/**
* Sorts an array of objects by a given key with an optional nested field.
* @param {Array} data - The array of objects to sort.
* @param {string} key - The key to sort by.
* @param {string} direction - The sort direction ('asc' or 'desc').
* @param {boolean} isNested - Whether the key is nested in an object.
* @returns {Array} - The sorted array.
*/
export function sortData(data, key, direction, isNested = false) {
return [...data].sort((a, b) => {
const aValue = isNested ? a.jobApplication[key]?.toLowerCase() : a[key]?.toLowerCase();
const bValue = isNested ? b.jobApplication[key]?.toLowerCase() : b[key]?.toLowerCase();

if (aValue < bValue) return direction === 'asc' ? -1 : 1;
if (aValue > bValue) return direction === 'asc' ? 1 : -1;
return 0;
});
}

0 comments on commit 0a0b6c7

Please sign in to comment.