diff --git a/apps/backend/src/auth/auth.service.ts b/apps/backend/src/auth/auth.service.ts
index ede503997..42bc42f3c 100644
--- a/apps/backend/src/auth/auth.service.ts
+++ b/apps/backend/src/auth/auth.service.ts
@@ -1,5 +1,6 @@
import {
ConflictException,
+ HttpException,
Injectable,
InternalServerErrorException,
Logger,
@@ -75,10 +76,18 @@ export class AuthService {
return sub ?? '';
} catch (error) {
- if (error instanceof Error && error.name == 'UsernameExistsException') {
+ if (error instanceof HttpException) {
+ throw error;
+ } else if (
+ error instanceof Error &&
+ error.name == 'UsernameExistsException'
+ ) {
throw new ConflictException('A user with this email already exists');
} else {
- throw new InternalServerErrorException('Failed to create user');
+ const reason = error instanceof Error ? error.message : String(error);
+ throw new InternalServerErrorException(
+ `Failed to create user: ${reason}`,
+ );
}
}
}
@@ -97,8 +106,9 @@ export class AuthService {
`Failed to add user ${username} to group ${groupName}`,
error,
);
+ const reason = error instanceof Error ? error.message : String(error);
throw new InternalServerErrorException(
- `Failed to add user to group ${groupName}`,
+ `Failed to add user to group ${groupName}: ${reason}`,
);
}
}
diff --git a/apps/backend/src/auth/dtos/sign-up.dto.ts b/apps/backend/src/auth/dtos/sign-up.dto.ts
index 6fa066a3f..59e13223f 100644
--- a/apps/backend/src/auth/dtos/sign-up.dto.ts
+++ b/apps/backend/src/auth/dtos/sign-up.dto.ts
@@ -16,8 +16,7 @@ export class SignUpDto {
@IsString()
@IsNotEmpty()
@IsPhoneNumber('US', {
- message:
- 'phone must be a valid phone number (make sure all the digits are correct)',
+ message: 'Phone must be a valid US phone number.',
})
phone!: string;
}
diff --git a/apps/backend/src/foodManufacturers/dtos/manufacturer-application.dto.ts b/apps/backend/src/foodManufacturers/dtos/manufacturer-application.dto.ts
index e5cacdad1..6c5460802 100644
--- a/apps/backend/src/foodManufacturers/dtos/manufacturer-application.dto.ts
+++ b/apps/backend/src/foodManufacturers/dtos/manufacturer-application.dto.ts
@@ -41,8 +41,7 @@ export class FoodManufacturerApplicationDto {
@IsString()
@IsNotEmpty()
@IsPhoneNumber('US', {
- message:
- 'contactPhone must be a valid phone number (make sure all the digits are correct)',
+ message: 'Phone must be a valid US phone number.',
})
contactPhone!: string;
@@ -67,8 +66,7 @@ export class FoodManufacturerApplicationDto {
@IsOptional()
@IsString()
@IsPhoneNumber('US', {
- message:
- 'secondaryContactPhone must be a valid phone number (make sure all the digits are correct)',
+ message: 'Secondary phone must be a valid US phone number.',
})
@IsNotEmpty()
secondaryContactPhone?: string;
diff --git a/apps/backend/src/foodManufacturers/dtos/update-manufacturer-application.dto.ts b/apps/backend/src/foodManufacturers/dtos/update-manufacturer-application.dto.ts
index 7130c690e..d291253ce 100644
--- a/apps/backend/src/foodManufacturers/dtos/update-manufacturer-application.dto.ts
+++ b/apps/backend/src/foodManufacturers/dtos/update-manufacturer-application.dto.ts
@@ -37,8 +37,7 @@ export class UpdateFoodManufacturerApplicationDto {
@IsOptional()
@IsString()
@IsPhoneNumber('US', {
- message:
- 'secondaryContactPhone must be a valid phone number (make sure all the digits are correct)',
+ message: 'Secondary phone contact must be a valid US phone number.',
})
@IsNotEmpty()
secondaryContactPhone?: string;
diff --git a/apps/backend/src/foodManufacturers/manufacturers.controller.ts b/apps/backend/src/foodManufacturers/manufacturers.controller.ts
index 92b4cd374..f60e3a4c4 100644
--- a/apps/backend/src/foodManufacturers/manufacturers.controller.ts
+++ b/apps/backend/src/foodManufacturers/manufacturers.controller.ts
@@ -159,7 +159,7 @@ export class FoodManufacturersController {
type: 'string',
format: 'phone',
example: '(508) 508-6789',
- description: 'Must be a valid US phone number',
+ description: 'Phone must be a valid US phone number',
},
secondaryContactFirstName: {
type: 'string',
@@ -178,7 +178,7 @@ export class FoodManufacturersController {
type: 'string',
format: 'phone',
example: '(508) 528-6789',
- description: 'Must be a valid US phone number',
+ description: 'Phone must be a valid US phone number',
},
unlistedProductAllergens: {
type: 'array',
diff --git a/apps/backend/src/pantries/dtos/pantry-application.dto.ts b/apps/backend/src/pantries/dtos/pantry-application.dto.ts
index d908dcf51..e3971bca8 100644
--- a/apps/backend/src/pantries/dtos/pantry-application.dto.ts
+++ b/apps/backend/src/pantries/dtos/pantry-application.dto.ts
@@ -40,8 +40,7 @@ export class PantryApplicationDto {
@IsString()
@IsNotEmpty()
@IsPhoneNumber('US', {
- message:
- 'contactPhone must be a valid phone number (make sure all the digits are correct)',
+ message: 'Phone must be a valid US phone number.',
})
contactPhone!: string;
@@ -74,8 +73,7 @@ export class PantryApplicationDto {
@IsOptional()
@IsString()
@IsPhoneNumber('US', {
- message:
- 'secondaryContactPhone must be a valid phone number (make sure all the digits are correct)',
+ message: 'Secondary phone must be a valid US phone number.',
})
@IsNotEmpty()
secondaryContactPhone?: string;
diff --git a/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts b/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts
index b744ca63b..632d10acd 100644
--- a/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts
+++ b/apps/backend/src/pantries/dtos/update-pantry-application.dto.ts
@@ -44,8 +44,7 @@ export class UpdatePantryApplicationDto {
@IsOptional()
@IsString()
@IsPhoneNumber('US', {
- message:
- 'Secondary contact phone must be a valid phone number (make sure all the digits are correct)',
+ message: 'Secondary phone must be a valid US phone number.',
})
@IsNotEmpty()
secondaryContactPhone?: string;
diff --git a/apps/backend/src/pantries/pantries.controller.ts b/apps/backend/src/pantries/pantries.controller.ts
index 913ee0998..daaf470e8 100644
--- a/apps/backend/src/pantries/pantries.controller.ts
+++ b/apps/backend/src/pantries/pantries.controller.ts
@@ -171,7 +171,7 @@ export class PantriesController {
type: 'string',
format: 'phone',
example: '(508) 508-6789',
- description: 'Must be a valid US phone number',
+ description: 'Phone must be a valid US phone number',
},
hasEmailContact: {
type: 'boolean',
@@ -198,7 +198,7 @@ export class PantriesController {
type: 'string',
format: 'phone',
example: '(508) 528-6789',
- description: 'Must be a valid US phone number',
+ description: 'Phone must be a valid US phone number',
},
pantryName: {
type: 'string',
diff --git a/apps/backend/src/users/dtos/update-user-info.dto.ts b/apps/backend/src/users/dtos/update-user-info.dto.ts
index 94716f917..45ec0f991 100644
--- a/apps/backend/src/users/dtos/update-user-info.dto.ts
+++ b/apps/backend/src/users/dtos/update-user-info.dto.ts
@@ -23,8 +23,7 @@ export class UpdateUserInfoDto {
@IsString()
@IsNotEmpty()
@IsPhoneNumber('US', {
- message:
- 'phone must be a valid phone number (make sure all the digits are correct)',
+ message: 'Phone must be a valid US phone number.',
})
phone?: string;
}
diff --git a/apps/backend/src/users/dtos/userSchema.dto.ts b/apps/backend/src/users/dtos/userSchema.dto.ts
index 2f110c0bd..5876eb470 100644
--- a/apps/backend/src/users/dtos/userSchema.dto.ts
+++ b/apps/backend/src/users/dtos/userSchema.dto.ts
@@ -27,8 +27,7 @@ export class userSchemaDto {
@IsString()
@IsNotEmpty()
@IsPhoneNumber('US', {
- message:
- 'phone must be a valid phone number (make sure all the digits are correct)',
+ message: 'Phone must be a valid US phone number.',
})
phone!: string;
diff --git a/apps/frontend/src/app.tsx b/apps/frontend/src/app.tsx
index cfaf7a919..50b375792 100644
--- a/apps/frontend/src/app.tsx
+++ b/apps/frontend/src/app.tsx
@@ -1,4 +1,8 @@
-import { createBrowserRouter, RouterProvider } from 'react-router-dom';
+import {
+ createBrowserRouter,
+ Navigate,
+ RouterProvider,
+} from 'react-router-dom';
import Root from '@containers/root';
import NotFound from '@containers/404';
import FormRequests from '@containers/formRequests';
@@ -9,7 +13,6 @@ import ApprovePantries from '@containers/approvePantries';
import PantryApplicationDetails from '@containers/pantryApplicationDetails';
import VolunteerManagement from '@containers/userManagement';
import AdminDonation from '@containers/adminDonation';
-import Homepage from '@containers/homepage';
import AdminOrderManagement from '@containers/adminOrderManagement';
import { Amplify } from 'aws-amplify';
import CognitoAuthConfig from './aws-exports';
@@ -50,7 +53,7 @@ const router = createBrowserRouter([
// Public routes (no auth needed)
{
index: true,
- element: ,
+ element: ,
},
{
path: ROUTES.LOGIN,
diff --git a/apps/frontend/src/assets/login_background.png b/apps/frontend/src/assets/login_background.png
index a45dd03c6..59617c0c4 100644
Binary files a/apps/frontend/src/assets/login_background.png and b/apps/frontend/src/assets/login_background.png differ
diff --git a/apps/frontend/src/components/Navbar.tsx b/apps/frontend/src/components/Navbar.tsx
index 856a0753c..c6a033478 100644
--- a/apps/frontend/src/components/Navbar.tsx
+++ b/apps/frontend/src/components/Navbar.tsx
@@ -228,7 +228,7 @@ const Navbar: React.FC = () => {
ApiClient.getMe()
.then(setCurrentUser)
.catch(() => setCurrentUser(null));
- } else {
+ } else if (authStatus === 'unauthenticated') {
setCurrentUser(null);
}
}, [authStatus]);
@@ -271,7 +271,6 @@ const Navbar: React.FC = () => {
navigate(ROUTES.LOGIN, { replace: true });
};
- // Should be changed once other dashboards are implmented
const ROLE_DASHBOARD_ROUTE: Record = {
[Role.ADMIN]: ROUTES.ADMIN_DASHBOARD,
[Role.FOODMANUFACTURER]: ROUTES.FM_DASHBOARD,
@@ -330,7 +329,7 @@ const Navbar: React.FC = () => {
overflow="hidden"
style={{ whiteSpace: 'normal', wordBreak: 'break-word' }}
>
- {roleLabel ? `${roleLabel} Dashboard` : 'Dashboard'}
+ {roleLabel ? `${roleLabel}` : 'Profile'}
{
-
-
= ({
);
const itemsPerPage = 10;
- const totalPages = Math.ceil(filteredRequests.length / itemsPerPage);
const paginatedRequests = filteredRequests.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage,
@@ -469,59 +466,14 @@ const RequestManagement: React.FC = ({
>
)}
- {totalPages > 1 && (
-
+ setCurrentPage(e.page)}
- >
-
-
- setCurrentPage((prev) => Math.max(prev - 1, 1))}
- >
-
-
-
-
- (
-
- {page.value}
-
- )}
- />
-
-
-
- setCurrentPage((prev) => Math.min(prev + 1, totalPages))
- }
- >
-
-
-
-
-
- )}
+ onPageChange={setCurrentPage}
+ />
+
);
};
diff --git a/apps/frontend/src/components/forms/addNewVolunteerModal.tsx b/apps/frontend/src/components/forms/addNewVolunteerModal.tsx
index 1c0e3e96e..f175d9dba 100644
--- a/apps/frontend/src/components/forms/addNewVolunteerModal.tsx
+++ b/apps/frontend/src/components/forms/addNewVolunteerModal.tsx
@@ -35,8 +35,10 @@ const NewVolunteerModal: React.FC = ({
const [isOpen, setIsOpen] = useState(false);
const [alertState, setAlertMessage] = useAlert();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async () => {
+ if (isSubmitting) return;
if (!firstName || !lastName || !email || !phone || phone === '+1') {
setAlertMessage('Please fill in all fields. *', AlertStatus.ERROR);
return;
@@ -50,6 +52,7 @@ const NewVolunteerModal: React.FC = ({
role: Role.VOLUNTEER,
};
+ setIsSubmitting(true);
try {
await ApiClient.postUser(newVolunteer);
if (onSubmitSuccess) onSubmitSuccess();
@@ -90,6 +93,8 @@ const NewVolunteerModal: React.FC = ({
if (onSubmitFail) onSubmitFail();
handleClear();
}
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -226,6 +231,7 @@ const NewVolunteerModal: React.FC = ({
bg={'blue.hover'}
color={'white'}
onClick={handleSubmit}
+ disabled={isSubmitting}
>
Submit
diff --git a/apps/frontend/src/components/forms/assignVolunteersModal.tsx b/apps/frontend/src/components/forms/assignVolunteersModal.tsx
index e4a4dffe4..120fab770 100644
--- a/apps/frontend/src/components/forms/assignVolunteersModal.tsx
+++ b/apps/frontend/src/components/forms/assignVolunteersModal.tsx
@@ -52,6 +52,8 @@ const AssignVolunteersModal: React.FC = ({
const [searchName, setSearchName] = useState('');
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
const handleSearchNameChange = (
event: React.ChangeEvent,
) => {
@@ -99,6 +101,8 @@ const AssignVolunteersModal: React.FC = ({
};
const handleSave = async () => {
+ if (isSubmitting) return;
+ setIsSubmitting(true);
try {
const originalIds = new Set(pantry.volunteers.map((v) => v.userId));
@@ -120,6 +124,8 @@ const AssignVolunteersModal: React.FC = ({
onClose();
} catch {
setAlertMessage('Error saving volunteer assignments', AlertStatus.ERROR);
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -273,6 +279,7 @@ const AssignVolunteersModal: React.FC = ({
fontWeight={600}
onClick={handleSave}
px={10}
+ disabled={isSubmitting}
>
Save Changes
diff --git a/apps/frontend/src/components/forms/createNewOrderModal.tsx b/apps/frontend/src/components/forms/createNewOrderModal.tsx
index 458f655fb..b9a1d3d8c 100644
--- a/apps/frontend/src/components/forms/createNewOrderModal.tsx
+++ b/apps/frontend/src/components/forms/createNewOrderModal.tsx
@@ -54,6 +54,7 @@ const CreateNewOrderModal: React.FC = ({
const [itemAllocations, setItemAllocations] = useState<
Record
>({});
+ const [isSubmittingOrder, setIsSubmittingOrder] = useState(false);
useEffect(() => {
const fetchManufacturers = async () => {
@@ -120,6 +121,7 @@ const CreateNewOrderModal: React.FC = ({
};
const onSubmitNewOrder = async () => {
+ if (isSubmittingOrder) return;
const cleanedAllocations: Record = {};
for (const item in itemAllocations) {
@@ -136,12 +138,15 @@ const CreateNewOrderModal: React.FC = ({
itemAllocations: cleanedAllocations,
};
+ setIsSubmittingOrder(true);
try {
await apiClient.createOrder(data);
onClose();
onSuccess();
} catch {
setAlertMessage('Error creating new order', AlertStatus.ERROR);
+ } finally {
+ setIsSubmittingOrder(false);
}
};
@@ -459,6 +464,7 @@ const CreateNewOrderModal: React.FC = ({
bg={'blue.hover'}
color={'white'}
onClick={onSubmitNewOrder}
+ disabled={isSubmittingOrder}
>
Continue
diff --git a/apps/frontend/src/components/forms/donationDetailsModal.tsx b/apps/frontend/src/components/forms/donationDetailsModal.tsx
index 018a39f0b..c22059f2c 100644
--- a/apps/frontend/src/components/forms/donationDetailsModal.tsx
+++ b/apps/frontend/src/components/forms/donationDetailsModal.tsx
@@ -1,4 +1,4 @@
-import React, { useState, useEffect, useCallback } from 'react';
+import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
Box,
Text,
@@ -47,8 +47,13 @@ const DonationDetailsModal: React.FC = ({
const [alertState, setAlertMessage] = useAlert();
const [isEditing, setIsEditing] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
- const donationId = donation?.donationId;
+ const donationRef = useRef(donation);
+ if (donation) donationRef.current = donation;
+ const displayDonation = donation ?? donationRef.current;
+
+ const donationId = displayDonation?.donationId;
const handleCancel = () => {
setIsEditing(false);
@@ -79,6 +84,7 @@ const DonationDetailsModal: React.FC = ({
foodRescue: r.foodRescue,
}));
+ setIsSubmitting(true);
try {
await ApiClient.editDonationItems(donationId, body);
await loadItems();
@@ -90,6 +96,8 @@ const DonationDetailsModal: React.FC = ({
'Donation items could not be updated.',
AlertStatus.ERROR,
);
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -124,7 +132,7 @@ const DonationDetailsModal: React.FC = ({
)}
- {donation !== null && (
+ {displayDonation !== null && (
= ({
Donation #{donationId} Stock
- {donation.status === DonationStatus.AVAILABLE &&
+ {displayDonation.status === DonationStatus.AVAILABLE &&
!isEditing && (
<>
= ({
)}
- {donation.foodManufacturer?.foodManufacturerName}
+ {displayDonation.foodManufacturer?.foodManufacturerName}
+
+
+ {formatDate(displayDonation.dateDonated)}
- {formatDate(donation.dateDonated)}
@@ -172,6 +182,7 @@ const DonationDetailsModal: React.FC = ({
onCancel={handleCancel}
onSubmit={handleUpdate}
submitButtonLabel="Update Donation"
+ isSubmitting={isSubmitting}
>
) : (
@@ -226,27 +237,28 @@ const DonationDetailsModal: React.FC = ({
)}
- {!isEditing && donation.recurrence !== RecurrenceEnum.NONE && (
-
-
- Donation sets up recurring reminders
-
+ {!isEditing &&
+ displayDonation.recurrence !== RecurrenceEnum.NONE && (
+
+
+ Donation sets up recurring reminders
+
- {donation.nextDonationDates &&
- donation.nextDonationDates.length > 0 && (
-
-
- Upcoming reminder emails
-
-
- {donation.nextDonationDates
- .map((date) => formatDate(date))
- .join(', ')}
-
-
- )}
-
- )}
+ {displayDonation.nextDonationDates &&
+ displayDonation.nextDonationDates.length > 0 && (
+
+
+ Upcoming reminder emails
+
+
+ {displayDonation.nextDonationDates
+ .map((date) => formatDate(date))
+ .join(', ')}
+
+
+ )}
+
+ )}
diff --git a/apps/frontend/src/components/forms/editableDonationItemsTable.tsx b/apps/frontend/src/components/forms/editableDonationItemsTable.tsx
index dbaad9cd3..2a9933d71 100644
--- a/apps/frontend/src/components/forms/editableDonationItemsTable.tsx
+++ b/apps/frontend/src/components/forms/editableDonationItemsTable.tsx
@@ -122,6 +122,7 @@ interface EditableDonationItemsTableProps {
onCancel: () => void;
onSubmit: (rows: DonationRow[], recurrence: RecurrenceData | null) => void;
submitButtonLabel: string;
+ isSubmitting?: boolean;
}
const EditableDonationItemsTable: React.FC = ({
@@ -130,6 +131,7 @@ const EditableDonationItemsTable: React.FC = ({
onCancel,
onSubmit,
submitButtonLabel,
+ isSubmitting,
}) => {
const [rows, setRows] = useState(initialRows ?? [BLANK_ROW]);
@@ -200,7 +202,7 @@ const EditableDonationItemsTable: React.FC = ({
endsAfter,
);
- const isSubmitDisabled = firstValidationError !== null;
+ const isSubmitDisabled = firstValidationError !== null || !!isSubmitting;
const getSelectedDaysText = () => {
const selected = (Object.keys(repeatOn) as DayOfWeek[]).filter(
@@ -672,7 +674,7 @@ const EditableDonationItemsTable: React.FC = ({
>
Cancel
-
+
diff --git a/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx b/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx
index 6b4c30024..86be4c609 100644
--- a/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx
+++ b/apps/frontend/src/components/forms/manufacturerApplicationForm.tsx
@@ -20,6 +20,7 @@ import {
Form,
redirect,
useActionData,
+ useNavigation,
} from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { USPhoneInput } from '@components/forms/usPhoneInput';
@@ -45,6 +46,8 @@ const ManufacturerApplicationForm: React.FC = () => {
>([]);
const [alertState, setAlertMessage] = useAlert();
const actionData = useActionData() as { error?: string } | undefined;
+ const navigation = useNavigation();
+ const isSubmitting = navigation.state === 'submitting';
const sectionTitleStyles = {
fontFamily: 'inter',
@@ -604,7 +607,12 @@ const ManufacturerApplicationForm: React.FC = () => {
>
Cancel
-
diff --git a/apps/frontend/src/components/forms/pantryApplicationForm.tsx b/apps/frontend/src/components/forms/pantryApplicationForm.tsx
index 69e3cc63f..e1781bfde 100644
--- a/apps/frontend/src/components/forms/pantryApplicationForm.tsx
+++ b/apps/frontend/src/components/forms/pantryApplicationForm.tsx
@@ -22,6 +22,7 @@ import {
Form,
redirect,
useActionData,
+ useNavigation,
} from 'react-router-dom';
import React, { useEffect, useState } from 'react';
import { USPhoneInput } from '@components/forms/usPhoneInput';
@@ -91,6 +92,8 @@ const PantryApplicationForm: React.FC = () => {
const [otherEmailContact, setOtherEmailContact] = useState(false);
const [alertState, setAlertMessage] = useAlert();
const actionData = useActionData() as { error?: string } | undefined;
+ const navigation = useNavigation();
+ const isSubmitting = navigation.state === 'submitting';
const sectionTitleStyles = {
fontFamily: 'inter',
@@ -1169,7 +1172,13 @@ const PantryApplicationForm: React.FC = () => {
>
Cancel
-
+
Submit Application
diff --git a/apps/frontend/src/components/forms/requestDetailsModal.tsx b/apps/frontend/src/components/forms/requestDetailsModal.tsx
index ed586a89b..58feeb433 100644
--- a/apps/frontend/src/components/forms/requestDetailsModal.tsx
+++ b/apps/frontend/src/components/forms/requestDetailsModal.tsx
@@ -25,20 +25,18 @@ import {
CloseButton,
Tabs,
Badge,
- Pagination,
- ButtonGroup,
- IconButton,
Button,
Textarea,
Input,
} from '@chakra-ui/react';
-import { ChevronRight, ChevronLeft, ChevronDownIcon } from 'lucide-react';
+import { ChevronDownIcon } from 'lucide-react';
import { TagGroup } from './tagGroup';
import { useGroupedItemsByFoodType } from '../../hooks/groupedItemsByFoodType';
import { useModalBodyCleanup } from '../../hooks/modalBodyCleanup';
import { useAlert } from '../../hooks/alert';
import { FloatingAlert } from '../floatingAlert';
import { EditButton, DeleteButton } from '@components/editDeleteButtons';
+import { PaginationControl } from '@components/pagination';
interface RequestDetailsModalProps {
request: FoodRequestSummaryDto;
@@ -664,56 +662,12 @@ const RequestDetailsModal: React.FC = ({
{orderDetailsList.length > 0 && (
- setCurrentPage(page)}
- >
-
-
-
- setCurrentPage((prev) =>
- Math.max(prev - 1, 1),
- )
- }
- >
-
-
-
-
- (
- setCurrentPage(page.value)}
- >
- {page.value}
-
- )}
- />
-
-
-
- setCurrentPage((prev) =>
- Math.min(prev + 1, orderDetailsList.length),
- )
- }
- >
-
-
-
-
-
+ onPageChange={setCurrentPage}
+ />
)}
diff --git a/apps/frontend/src/components/forms/requestFormModal.tsx b/apps/frontend/src/components/forms/requestFormModal.tsx
index 987a65d5e..e27af19b6 100644
--- a/apps/frontend/src/components/forms/requestFormModal.tsx
+++ b/apps/frontend/src/components/forms/requestFormModal.tsx
@@ -49,6 +49,7 @@ const FoodRequestFormModal: React.FC = ({
const [feedbackOnPriorDonation, setFeedbackOnPriorDonation] =
useState('');
const [alertState, setAlertMessage] = useAlert();
+ const [isSubmitting, setIsSubmitting] = useState(false);
const isFormValid =
requestedSize !== '' &&
@@ -88,6 +89,7 @@ const FoodRequestFormModal: React.FC = ({
}, [isOpen, previousRequest, setAlertMessage]);
const handleSubmit = async () => {
+ if (isSubmitting) return;
const foodRequestData: CreateFoodRequestBody = {
pantryId,
requestedSize: requestedSize as RequestSize,
@@ -97,6 +99,7 @@ const FoodRequestFormModal: React.FC = ({
requestedFoodTypes: selectedFoodTypes,
};
+ setIsSubmitting(true);
try {
await apiClient.createFoodRequest(foodRequestData);
setAlertMessage('Request submitted', AlertStatus.INFO);
@@ -104,6 +107,8 @@ const FoodRequestFormModal: React.FC = ({
onSuccess();
} catch {
setAlertMessage('Request could not be submitted.', AlertStatus.ERROR);
+ } finally {
+ setIsSubmitting(false);
}
};
@@ -408,7 +413,7 @@ const FoodRequestFormModal: React.FC = ({
onClick={handleSubmit}
bg={isFormValid ? 'blue.hover' : 'neutral.400'}
color={'white'}
- disabled={!isFormValid}
+ disabled={!isFormValid || isSubmitting}
>
Continue
diff --git a/apps/frontend/src/components/pagination.tsx b/apps/frontend/src/components/pagination.tsx
new file mode 100644
index 000000000..b196532e4
--- /dev/null
+++ b/apps/frontend/src/components/pagination.tsx
@@ -0,0 +1,62 @@
+import React from 'react';
+import { Pagination, ButtonGroup, IconButton } from '@chakra-ui/react';
+import { ChevronLeft, ChevronRight } from 'lucide-react';
+
+interface PaginationControlProps {
+ count: number;
+ pageSize: number;
+ page: number;
+ onPageChange: (page: number) => void;
+}
+
+export const PaginationControl: React.FC = ({
+ count,
+ pageSize,
+ page,
+ onPageChange,
+}) => {
+ const totalPages = Math.ceil(count / pageSize);
+
+ if (totalPages <= 1) return null;
+
+ return (
+ onPageChange(e.page)}
+ >
+
+
+
+
+
+
+
+ (
+
+ {p.value}
+
+ )}
+ />
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/apps/frontend/src/containers/adminDonation.tsx b/apps/frontend/src/containers/adminDonation.tsx
index 3d3a98e4a..680e48ac2 100644
--- a/apps/frontend/src/containers/adminDonation.tsx
+++ b/apps/frontend/src/containers/adminDonation.tsx
@@ -1,15 +1,12 @@
import React, { useState, useEffect } from 'react';
-import { ArrowDownUp, ChevronRight, ChevronLeft, Funnel } from 'lucide-react';
+import { ArrowDownUp, Funnel } from 'lucide-react';
import {
Box,
Button,
Table,
Heading,
- Pagination,
- IconButton,
Checkbox,
VStack,
- ButtonGroup,
Link,
} from '@chakra-ui/react';
import { AlertStatus, Donation } from '../types/types';
@@ -22,6 +19,7 @@ import { useSearchParams, useNavigate } from 'react-router-dom';
import { ROUTES } from '../routes';
import FMDeleteDonationActionModal from '@components/forms/fmDeleteDonationModal';
import PageEmptyState from '@components/pageEmptyState';
+import { PaginationControl } from '@components/pagination';
const AdminDonation: React.FC = () => {
const [searchParams] = useSearchParams();
@@ -80,9 +78,10 @@ const AdminDonation: React.FC = () => {
setCurrentPage(Math.floor(idx / itemsPerPage) + 1);
}
} else {
+ setAlertMessage('Donation not found.', AlertStatus.ERROR);
navigate(ROUTES.ADMIN_DONATION, { replace: true });
}
- }, [searchParams, donations, navigate]);
+ }, [searchParams, donations, navigate, setAlertMessage]);
// Pre-fill manufacturer filter from the foodManufacturerId url param and then
// clear the param, so navigating from "View Donations" filters to that
@@ -146,7 +145,6 @@ const AdminDonation: React.FC = () => {
);
const itemsPerPage = 10;
- const totalPages = Math.ceil(filteredDonations.length / itemsPerPage);
const paginatedDonations = filteredDonations.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage,
@@ -258,72 +256,79 @@ const AdminDonation: React.FC = () => {
Sort
-
-
-
-
- Donation #
-
-
- Manufacturer
-
-
- Date Started
-
-
-
-
- {paginatedDonations.map((donation, index) => (
-
-
+ ) : (
+
+
+
+
- setSelectedDonation(donation)}
- >
- {donation.donationId}
-
-
-
+
- {donation.foodManufacturer?.foodManufacturerName}
-
-
+
- {formatDate(donation.dateDonated)}
-
+ Date Started
+
- ))}
-
-
+
+
+ {paginatedDonations.map((donation, index) => (
+
+
+ setSelectedDonation(donation)}
+ >
+ {donation.donationId}
+
+
+
+ {donation.foodManufacturer?.foodManufacturerName}
+
+
+ {formatDate(donation.dateDonated)}
+
+
+ ))}
+
+
+ )}
>
)}
@@ -341,68 +346,25 @@ const AdminDonation: React.FC = () => {
}}
/>
- {selectedDonation !== null && (
- {
- setSelectedDonation(null);
- navigate(ROUTES.ADMIN_DONATION, { replace: true });
- }}
- onSuccess={() => fetchDonations()}
- onDelete={() => setDeleteDonation(selectedDonation)}
- />
- )}
+ {
+ setSelectedDonation(null);
+ navigate(ROUTES.ADMIN_DONATION, { replace: true });
+ }}
+ onSuccess={() => fetchDonations()}
+ onDelete={() => setDeleteDonation(selectedDonation)}
+ />
- {totalPages > 1 && (
-
+ setCurrentPage(e.page)}
- >
-
-
-
-
-
- (
-
- {page.value}
-
- )}
- />
-
-
-
-
-
-
- )}
+ onPageChange={setCurrentPage}
+ />
+
);
};
diff --git a/apps/frontend/src/containers/adminDonationStats.tsx b/apps/frontend/src/containers/adminDonationStats.tsx
index 80911d06c..1902cde5c 100644
--- a/apps/frontend/src/containers/adminDonationStats.tsx
+++ b/apps/frontend/src/containers/adminDonationStats.tsx
@@ -1,27 +1,19 @@
-import React, { useState, useEffect } from 'react';
-import {
- ChevronRight,
- ChevronLeft,
- ChevronDown,
- Funnel,
- Search,
-} from 'lucide-react';
+import React, { useState, useEffect, useRef } from 'react';
+import { ChevronDown, Funnel, Search } from 'lucide-react';
import {
Box,
Button,
Table,
Heading,
- Pagination,
- IconButton,
Checkbox,
VStack,
- ButtonGroup,
Input,
} from '@chakra-ui/react';
import { AlertStatus, PantryStats, TotalStats } from '../types/types';
import ApiClient from '@api/apiClient';
import { FloatingAlert } from '@components/floatingAlert';
import { useAlert } from '../hooks/alert';
+import { PaginationControl } from '@components/pagination';
const AdminDonationStats: React.FC = () => {
// Individual and combined pantry stats to be displayed
@@ -40,6 +32,9 @@ const AdminDonationStats: React.FC = () => {
const [alertState, setAlertMessage] = useAlert();
+ const totalStatsRequestIdRef = useRef(0);
+ const pantryStatsRequestIdRef = useRef(0);
+
useEffect(() => {
const fetchInitialData = async () => {
try {
@@ -60,23 +55,26 @@ const AdminDonationStats: React.FC = () => {
}, [setAlertMessage]);
useEffect(() => {
- // Total stats only displayed on first page, so no need to do anything on page change
- if (currentPage !== 1) return;
-
+ const requestId = ++totalStatsRequestIdRef.current;
const fetchTotalStats = async () => {
try {
const stats = await ApiClient.getTotalStats(
selectedYears.length ? selectedYears : undefined,
);
- setTotalStats(stats);
+ if (requestId === totalStatsRequestIdRef.current) {
+ setTotalStats(stats);
+ }
} catch {
- setAlertMessage('Error fetching total stats', AlertStatus.ERROR);
+ if (requestId === totalStatsRequestIdRef.current) {
+ setAlertMessage('Error fetching total stats', AlertStatus.ERROR);
+ }
}
};
fetchTotalStats();
- }, [setAlertMessage, selectedYears, currentPage]);
+ }, [setAlertMessage, selectedYears]);
useEffect(() => {
+ const requestId = ++pantryStatsRequestIdRef.current;
const fetchStats = async () => {
try {
const stats = await ApiClient.getPantryStats({
@@ -84,9 +82,13 @@ const AdminDonationStats: React.FC = () => {
years: selectedYears.length ? selectedYears : undefined,
page: currentPage,
});
- setPantryStats(stats);
+ if (requestId === pantryStatsRequestIdRef.current) {
+ setPantryStats(stats);
+ }
} catch {
- setAlertMessage('Error fetching pantry stats', AlertStatus.ERROR);
+ if (requestId === pantryStatsRequestIdRef.current) {
+ setAlertMessage('Error fetching pantry stats', AlertStatus.ERROR);
+ }
}
};
fetchStats();
@@ -121,7 +123,6 @@ const AdminDonationStats: React.FC = () => {
const pantryList =
selectedPantries.length > 0 ? selectedPantries : pantryNameOptions;
const totalCount = pantryList.length;
- const totalPages = Math.ceil(totalCount / itemsPerPage);
const tableHeaderStyles = {
borderBottom: '1px solid',
@@ -409,85 +410,83 @@ const AdminDonationStats: React.FC = () => {
- {currentPage === 1 && (
-
-
- All Pantries
-
-
- {totalStats?.totalItems ?? 0}
-
-
- {(totalStats?.totalOz ?? 0).toFixed(2)}
-
-
- {(totalStats?.totalLbs ?? 0).toFixed(2)}
-
-
- ${(totalStats?.totalDonatedFoodValue ?? 0).toFixed(2)}
-
-
- ${(totalStats?.totalShippingCost ?? 0).toFixed(2)}
-
-
- ${(totalStats?.totalShippingCostPaidBySsf ?? 0).toFixed(2)}
-
-
- ${(totalStats?.totalValue ?? 0).toFixed(2)}
-
-
- {(totalStats?.percentageFoodRescueItems ?? 0).toFixed(2)}%
-
-
- {(totalStats?.foodRescueLbs ?? 0).toFixed(2)}
-
-
- )}
+
+
+ All Pantries
+
+
+ {totalStats?.totalItems ?? 0}
+
+
+ {(totalStats?.totalOz ?? 0).toFixed(2)}
+
+
+ {(totalStats?.totalLbs ?? 0).toFixed(2)}
+
+
+ ${(totalStats?.totalDonatedFoodValue ?? 0).toFixed(2)}
+
+
+ ${(totalStats?.totalShippingCost ?? 0).toFixed(2)}
+
+
+ ${(totalStats?.totalShippingCostPaidBySsf ?? 0).toFixed(2)}
+
+
+ ${(totalStats?.totalValue ?? 0).toFixed(2)}
+
+
+ {(totalStats?.percentageFoodRescueItems ?? 0).toFixed(2)}%
+
+
+ {(totalStats?.foodRescueLbs ?? 0).toFixed(2)}
+
+
{pantryStats.map((stat) => (
{
- {totalPages > 1 && (
-
+ setCurrentPage(e.page)}
- >
-
-
- setCurrentPage((prev) => Math.max(prev - 1, 1))}
- >
-
-
-
-
- (
- setCurrentPage(page.value)}
- >
- {page.value}
-
- )}
- />
-
-
-
- setCurrentPage((prev) => Math.min(prev + 1, totalPages))
- }
- >
-
-
-
-
-
- )}
+ onPageChange={setCurrentPage}
+ />
+
);
};
diff --git a/apps/frontend/src/containers/adminFoodManufacturerManagement.tsx b/apps/frontend/src/containers/adminFoodManufacturerManagement.tsx
index ec4d39f91..7484dffea 100644
--- a/apps/frontend/src/containers/adminFoodManufacturerManagement.tsx
+++ b/apps/frontend/src/containers/adminFoodManufacturerManagement.tsx
@@ -6,15 +6,12 @@ import {
Input,
VStack,
Box,
- Pagination,
- ButtonGroup,
- IconButton,
Link,
Button,
Checkbox,
Badge,
} from '@chakra-ui/react';
-import { ChevronRight, ChevronLeft, Funnel, Search } from 'lucide-react';
+import { Funnel, Search } from 'lucide-react';
import { AlertStatus, FoodManufacturer } from '../types/types';
import { DonateWastedFood } from '../types/manufacturerEnums';
import ApiClient from '@api/apiClient';
@@ -22,6 +19,7 @@ import { FloatingAlert } from '@components/floatingAlert';
import { useAlert } from '../hooks/alert';
import { useNavigate } from 'react-router-dom';
import { ROUTES } from '../routes';
+import { PaginationControl } from '@components/pagination';
const AdminFoodManufacturerManagement: React.FC = () => {
const navigate = useNavigate();
@@ -292,60 +290,12 @@ const AdminFoodManufacturerManagement: React.FC = () => {
- setCurrentPage(page)}
- >
-
-
-
- setCurrentPage((prev) => Math.max(prev - 1, 1))
- }
- >
-
-
-
-
- (
- setCurrentPage(page.value)}
- >
- {page.value}
-
- )}
- />
-
-
-
- setCurrentPage((prev) =>
- Math.min(
- prev + 1,
- Math.ceil(filteredFMs.length / pageSize),
- ),
- )
- }
- >
-
-
-
-
-
+ onPageChange={setCurrentPage}
+ />
diff --git a/apps/frontend/src/containers/adminOrderManagement.tsx b/apps/frontend/src/containers/adminOrderManagement.tsx
index 25940eaea..ea1d26f27 100644
--- a/apps/frontend/src/containers/adminOrderManagement.tsx
+++ b/apps/frontend/src/containers/adminOrderManagement.tsx
@@ -1,26 +1,15 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
import {
Box,
Button,
Table,
Heading,
- Pagination,
- IconButton,
VStack,
- ButtonGroup,
Checkbox,
Input,
Link,
} from '@chakra-ui/react';
-import {
- ArrowDownUp,
- ChevronRight,
- ChevronLeft,
- Funnel,
- Mail,
- CircleCheck,
- Search,
-} from 'lucide-react';
+import { ArrowDownUp, Funnel, Mail, CircleCheck, Search } from 'lucide-react';
import {
formatDate,
getInitials,
@@ -35,6 +24,7 @@ import { FloatingAlert } from '@components/floatingAlert';
import { useAlert } from '../hooks/alert';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { ROUTES } from '../routes';
+import { PaginationControl } from '@components/pagination';
// Extending the OrderSummary type to include assignee color for display
type OrderWithColor = OrderSummary & { assigneeColor?: string };
@@ -106,49 +96,49 @@ const AdminOrderManagement: React.FC = () => {
const MAX_PER_STATUS = 5;
- useEffect(() => {
- // Fetch all orders on component mount and sorts them into their appropriate status lists
- const fetchOrders = async () => {
- try {
- const data = await ApiClient.getAllOrders();
-
- const grouped: Record = {
- [OrderStatus.SHIPPED]: [],
- [OrderStatus.PENDING]: [],
- [OrderStatus.DELIVERED]: [],
- [OrderStatus.CLOSED]: [],
- };
-
- for (const order of data) {
- const status = order.status;
- const orderWithColor: OrderWithColor = { ...order };
-
- if (order.assignee) {
- orderWithColor.assigneeColor =
- USER_ICON_COLORS[order.assignee.id % USER_ICON_COLORS.length];
- }
-
- grouped[status].push(orderWithColor);
+ // Fetches all orders and sorts them into their appropriate status lists
+ const fetchOrders = useCallback(async () => {
+ try {
+ const data = await ApiClient.getAllOrders();
+
+ const grouped: Record = {
+ [OrderStatus.SHIPPED]: [],
+ [OrderStatus.PENDING]: [],
+ [OrderStatus.DELIVERED]: [],
+ [OrderStatus.CLOSED]: [],
+ };
+
+ for (const order of data) {
+ const status = order.status;
+ const orderWithColor: OrderWithColor = { ...order };
+
+ if (order.assignee) {
+ orderWithColor.assigneeColor =
+ USER_ICON_COLORS[order.assignee.id % USER_ICON_COLORS.length];
}
- setStatusOrders(grouped);
-
- // Initialize current page for each status
- const initialPages: Record = {
- [OrderStatus.SHIPPED]: 1,
- [OrderStatus.PENDING]: 1,
- [OrderStatus.DELIVERED]: 1,
- [OrderStatus.CLOSED]: 1,
- };
- setCurrentPages(initialPages);
- } catch {
- setAlertMessage('Error fetching orders', AlertStatus.ERROR);
+ grouped[status].push(orderWithColor);
}
- };
- fetchOrders();
+ setStatusOrders(grouped);
+
+ // Initialize current page for each status
+ const initialPages: Record = {
+ [OrderStatus.SHIPPED]: 1,
+ [OrderStatus.PENDING]: 1,
+ [OrderStatus.DELIVERED]: 1,
+ [OrderStatus.CLOSED]: 1,
+ };
+ setCurrentPages(initialPages);
+ } catch {
+ setAlertMessage('Error fetching orders', AlertStatus.ERROR);
+ }
}, [setAlertMessage]);
+ useEffect(() => {
+ fetchOrders();
+ }, [fetchOrders]);
+
// Helper to reset page for a specific status
const resetPageForStatus = (status: OrderStatus) => {
setCurrentPages((prev) => ({ ...prev, [status]: 1 }));
@@ -188,9 +178,10 @@ const AdminOrderManagement: React.FC = () => {
}
}
} else {
+ setAlertMessage('Order not found.', AlertStatus.ERROR);
navigate(ROUTES.ADMIN_ORDER_MANAGEMENT, { replace: true });
}
- }, [searchParams, statusOrders, navigate]);
+ }, [searchParams, statusOrders, navigate, setAlertMessage]);
// Pre-fill pantry filter from url param and then clear the param.
useEffect(() => {
@@ -314,6 +305,7 @@ const AdminOrderManagement: React.FC = () => {
setSelectedOrderId(null);
navigate(ROUTES.ADMIN_ORDER_MANAGEMENT, { replace: true });
}}
+ onSuccess={fetchOrders}
/>
);
@@ -356,7 +348,6 @@ const OrderStatusSection: React.FC = ({
const [isSortOpen, setIsSortOpen] = useState(false);
const MAX_PER_STATUS = 5;
- const totalPages = Math.ceil(totalOrders / MAX_PER_STATUS);
const handleFilterChange = (pantry: string, checked: boolean) => {
const newSelected = checked
@@ -650,251 +641,205 @@ const OrderStatusSection: React.FC = ({
- No Orders
+ {filterState.selectedPantries.length > 0
+ ? 'No Matching Orders'
+ : 'No Orders'}
- You have no {ORDER_STATUS_LABELS[status].toLowerCase()} orders
- at this time.
+ {filterState.selectedPantries.length > 0
+ ? 'No orders match the selected filter.'
+ : `You have no ${ORDER_STATUS_LABELS[
+ status
+ ].toLowerCase()} orders at this time.`}
) : (
<>
-
-
-
-
- Order #
-
-
- Status
-
-
- Assignee
-
-
- Pantry
-
-
- Dates
-
-
- Action Required
-
-
-
-
- {orders.map((order, index) => {
- const pantry = order.request.pantry;
-
- return (
-
+
+
+
+
+ Order #
+
+
+ Status
+
+
+ Assignee
+
+
-
+
+ Dates
+
+
+ Action Required
+
+
+
+
+ {orders.map((order, index) => {
+ const pantry = order.request.pantry;
+
+ return (
+
- onOrderSelect(order.orderId)}
+
- {order.orderId}
-
-
-
- onOrderSelect(order.orderId)}
+ >
+ {order.orderId}
+
+
+
- {ORDER_STATUS_LABELS[order.status]}
-
-
-
-
+ {ORDER_STATUS_LABELS[order.status]}
+
+
+
- {order.assignee ? (
-
- {getInitials(
- order.assignee.firstName,
- order.assignee.lastName,
- )}
-
- ) : (
-
- —
-
- )}
-
-
-
- {pantry.pantryName}
-
-
- {formatDate(order.createdAt)}-
- {order.deliveredAt && formatDate(order.deliveredAt)}
-
-
-
- );
- })}
-
-
-
- {totalPages > 1 && (
-
- onPageChange(e.page)}
- >
-
-
-
-
-
- (
-
+ {order.assignee ? (
+
+ {getInitials(
+ order.assignee.firstName,
+ order.assignee.lastName,
+ )}
+
+ ) : (
+
+ —
+
+ )}
+
+
+
- {page.value}
-
- )}
- />
+ {pantry.pantryName}
+
+
+ {formatDate(order.createdAt)}-
+ {order.deliveredAt && formatDate(order.deliveredAt)}
+
+
+
+ );
+ })}
+
+
+
-
-
-
-
-
-
- )}
+
+
+
>
)}
>
diff --git a/apps/frontend/src/containers/adminPantryManagement.tsx b/apps/frontend/src/containers/adminPantryManagement.tsx
index a6baea579..779ef3d6a 100644
--- a/apps/frontend/src/containers/adminPantryManagement.tsx
+++ b/apps/frontend/src/containers/adminPantryManagement.tsx
@@ -6,15 +6,12 @@ import {
Input,
VStack,
Box,
- Pagination,
- ButtonGroup,
- IconButton,
Link,
Button,
Checkbox,
Badge,
} from '@chakra-ui/react';
-import { ChevronRight, ChevronLeft, Funnel, Search } from 'lucide-react';
+import { Funnel, Search } from 'lucide-react';
import { AlertStatus, ApprovedPantryResponse } from '../types/types';
import ApiClient from '@api/apiClient';
import { FloatingAlert } from '@components/floatingAlert';
@@ -24,6 +21,7 @@ import { RefrigeratedDonation } from '../types/pantryEnums';
import AssignVolunteersModal from '@components/forms/assignVolunteersModal';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { ROUTES } from '../routes';
+import { PaginationControl } from '@components/pagination';
const AdminPantryManagement: React.FC = () => {
const navigate = useNavigate();
@@ -259,238 +257,218 @@ const AdminPantryManagement: React.FC = () => {
)}
-
-
-
-
- Pantry
-
-
- Assignee
-
-
- Refrigerator-Friendly
-
-
- Action
-
-
-
-
- {paginatedPantries?.map((pantry) => (
-
-
-
- navigate(
- ROUTES.PANTRY_MANAGEMENT_DETAILS.replace(
- ':pantryId',
- pantry.pantryId.toString(),
- ),
- )
- }
+
+ {selectedPantries.length > 0
+ ? 'No Matching Pantries'
+ : 'No Pantries'}
+
+
+ {selectedPantries.length > 0
+ ? 'No pantries match the selected filter.'
+ : 'You have no pantries at this time.'}
+
+
+ ) : (
+
+
+
+
+ Pantry
+
+
+ Assignee
+
+
+ Refrigerator-Friendly
+
+
+ Action
+
+
+
+
+ {paginatedPantries?.map((pantry) => (
+
+
+
+ navigate(
+ ROUTES.PANTRY_MANAGEMENT_DETAILS.replace(
+ ':pantryId',
+ pantry.pantryId.toString(),
+ ),
+ )
+ }
+ >
+ {pantry.pantryName}
+
+
+ setSelectedPantryToAssignVolunteers(pantry)}
+ cursor="pointer"
+ _hover={{ bg: 'gray.50' }}
>
- {pantry.pantryName}
-
-
- setSelectedPantryToAssignVolunteers(pantry)}
- cursor="pointer"
- _hover={{ bg: 'gray.50' }}
- >
-
- {pantry.volunteers && pantry.volunteers.length > 0 ? (
- (() => {
- const volunteers = pantry.volunteers.filter(
- (volunteer) => volunteer.active,
- );
- const maxVisible = 3;
+
+ {pantry.volunteers &&
+ pantry.volunteers.some(
+ (volunteer) => volunteer.active,
+ ) ? (
+ (() => {
+ const volunteers = pantry.volunteers.filter(
+ (volunteer) => volunteer.active,
+ );
+ const maxVisible = 3;
- const hasOverflow = volunteers.length > maxVisible;
- const visibleVolunteers = hasOverflow
- ? volunteers.slice(0, maxVisible - 1)
- : volunteers;
+ const hasOverflow = volunteers.length > maxVisible;
+ const visibleVolunteers = hasOverflow
+ ? volunteers.slice(0, maxVisible - 1)
+ : volunteers;
- const remainingCount =
- volunteers.length - (maxVisible - 1);
+ const remainingCount =
+ volunteers.length - (maxVisible - 1);
- return (
- <>
- {visibleVolunteers.map((volunteer, index) => (
-
- {getInitials(
- volunteer.firstName,
- volunteer.lastName,
- )}
-
- ))}
+ return (
+ <>
+ {visibleVolunteers.map((volunteer, index) => (
+
+ {getInitials(
+ volunteer.firstName,
+ volunteer.lastName,
+ )}
+
+ ))}
- {hasOverflow && (
-
- +{remainingCount}
-
- )}
- >
- );
- })()
- ) : (
-
- No Volunteer
-
- )}
-
-
-
-
- {pantry.refrigeratedDonation === RefrigeratedDonation.NO
- ? 'Not Refrigerator-Friendly'
- : 'Refrigerator/Freezer-Friendly'}
-
-
-
-
- navigate(
- `${ROUTES.ADMIN_ORDER_MANAGEMENT}?pantryId=${pantry.pantryId}`,
- )
- }
- >
- View Orders
-
-
-
- ))}
- {selectedPantryToAssignVolunteers && (
- setSelectedPantryToAssignVolunteers(null)}
- onSuccess={handleAssignVolunteersSuccess}
- isOpen={true}
- />
- )}
-
-
+ {hasOverflow && (
+
+ +{remainingCount}
+
+ )}
+ >
+ );
+ })()
+ ) : (
+
+ No Volunteer
+
+ )}
+
+
+
+
+ {pantry.refrigeratedDonation === RefrigeratedDonation.NO
+ ? 'Not Refrigerator-Friendly'
+ : 'Refrigerator/Freezer-Friendly'}
+
+
+
+
+ navigate(
+ `${ROUTES.ADMIN_ORDER_MANAGEMENT}?pantryId=${pantry.pantryId}`,
+ )
+ }
+ >
+ View Orders
+
+
+
+ ))}
+ {selectedPantryToAssignVolunteers && (
+ setSelectedPantryToAssignVolunteers(null)}
+ onSuccess={handleAssignVolunteersSuccess}
+ isOpen={true}
+ />
+ )}
+
+
+ )}
- setCurrentPage(page)}
- >
-
-
-
- setCurrentPage((prev) => Math.max(prev - 1, 1))
- }
- >
-
-
-
-
- (
- setCurrentPage(page.value)}
- >
- {page.value}
-
- )}
- />
-
-
-
- setCurrentPage((prev) =>
- Math.min(
- prev + 1,
- Math.ceil(filteredPantries.length / pageSize),
- ),
- )
- }
- >
-
-
-
-
-
+ onPageChange={setCurrentPage}
+ />
diff --git a/apps/frontend/src/containers/approveFoodManufacturers.tsx b/apps/frontend/src/containers/approveFoodManufacturers.tsx
index 0bffd7529..b464237f2 100644
--- a/apps/frontend/src/containers/approveFoodManufacturers.tsx
+++ b/apps/frontend/src/containers/approveFoodManufacturers.tsx
@@ -7,22 +7,14 @@ import {
Heading,
VStack,
Checkbox,
- Pagination,
- ButtonGroup,
- IconButton,
Link,
} from '@chakra-ui/react';
import ApiClient from '@api/apiClient';
import { AlertStatus, FoodManufacturer } from '../types/types';
-import {
- ArrowDownUp,
- ChevronLeft,
- ChevronRight,
- CircleCheck,
- Funnel,
-} from 'lucide-react';
+import { ArrowDownUp, CircleCheck, CircleX, Funnel } from 'lucide-react';
import { useAlert } from '../hooks/alert';
import { FloatingAlert } from '@components/floatingAlert';
+import { PaginationControl } from '@components/pagination';
import { ROUTES } from '../routes';
const ApproveFoodManufacturers: React.FC = () => {
@@ -30,6 +22,7 @@ const ApproveFoodManufacturers: React.FC = () => {
const [foodManufacturers, setFoodManufacturers] = useState<
FoodManufacturer[]
>([]);
+ const [hasError, setHasError] = useState(false);
const [sortAsc, setSortAsc] = useState(false);
const [selectedFoodManufacturers, setSelectedFoodManufacturers] = useState<
string[]
@@ -44,7 +37,9 @@ const ApproveFoodManufacturers: React.FC = () => {
try {
const data = await ApiClient.getAllPendingFoodManufacturers();
setFoodManufacturers(data);
+ setHasError(false);
} catch {
+ setHasError(true);
setAlertMessage('Error fetching food manufacturers', AlertStatus.ERROR);
}
};
@@ -91,7 +86,6 @@ const ApproveFoodManufacturers: React.FC = () => {
);
const itemsPerPage = 10;
- const totalPages = Math.ceil(filteredFoodManufacturers.length / itemsPerPage);
const paginatedFoodManufacturers = filteredFoodManufacturers.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage,
@@ -134,7 +128,7 @@ const ApproveFoodManufacturers: React.FC = () => {
timeout={6000}
/>
)}
- {foodManufacturers.length === 0 ? (
+ {!hasError && foodManufacturers.length === 0 ? (
{
There are no applications to review at this time
+ ) : hasError ? (
+
+
+
+
+
+ Unable to Load Applications
+
+
+ Something went wrong while loading applications. Please try again
+ later.
+
+
) : (
@@ -242,148 +261,142 @@ const ApproveFoodManufacturers: React.FC = () => {
Sort
-
-
-
-
- Application #
-
-
- Food Manufacturer
-
-
- Date Applied
-
-
- Actions
-
-
-
-
- {paginatedFoodManufacturers.map((foodManufacturer, index) => (
-
-
+
+
+
+
+ No Matching Applications
+
+
+ No applications match the selected filter.
+
+
+ ) : (
+
+
+
+
- {foodManufacturer.foodManufacturerId}
-
-
+
- {foodManufacturer.foodManufacturerName}
-
-
+
- {new Date(foodManufacturer.dateApplied).toLocaleDateString(
- 'en-US',
- {
+ Date Applied
+
+
+ Actions
+
+
+
+
+ {paginatedFoodManufacturers.map((foodManufacturer, index) => (
+
+
+ {foodManufacturer.foodManufacturerId}
+
+
+ {foodManufacturer.foodManufacturerName}
+
+
+ {new Date(
+ foodManufacturer.dateApplied,
+ ).toLocaleDateString('en-US', {
month: '2-digit',
day: '2-digit',
year: 'numeric',
- },
- )}
-
-
-
+
- View Details
-
-
-
- ))}
-
-
+ {
+ e.preventDefault();
+ navigate(
+ ROUTES.FOOD_MANUFACTURER_APPLICATION_DETAILS.replace(
+ ':applicationId',
+ String(foodManufacturer.foodManufacturerId),
+ ),
+ );
+ }}
+ >
+ View Details
+
+
+
+ ))}
+
+
+ )}
- {totalPages > 1 && (
-
+ setCurrentPage(e.page)}
- >
-
-
-
-
-
- (
-
- {page.value}
-
- )}
- />
-
-
-
-
-
-
- )}
+ onPageChange={setCurrentPage}
+ />
+
)}
diff --git a/apps/frontend/src/containers/approvePantries.tsx b/apps/frontend/src/containers/approvePantries.tsx
index 6fcca178f..6653c3146 100644
--- a/apps/frontend/src/containers/approvePantries.tsx
+++ b/apps/frontend/src/containers/approvePantries.tsx
@@ -7,27 +7,20 @@ import {
Heading,
VStack,
Checkbox,
- Pagination,
- ButtonGroup,
- IconButton,
Link,
} from '@chakra-ui/react';
import ApiClient from '@api/apiClient';
import { AlertStatus, Pantry } from '../types/types';
-import {
- ArrowDownUp,
- ChevronLeft,
- ChevronRight,
- CircleCheck,
- Funnel,
-} from 'lucide-react';
+import { ArrowDownUp, CircleCheck, CircleX, Funnel } from 'lucide-react';
import { useAlert } from '../hooks/alert';
import { FloatingAlert } from '@components/floatingAlert';
+import { PaginationControl } from '@components/pagination';
import { ROUTES } from '../routes';
const ApprovePantries: React.FC = () => {
const navigate = useNavigate();
const [pantries, setPantries] = useState([]);
+ const [hasError, setHasError] = useState(false);
const [sortAsc, setSortAsc] = useState(false);
const [selectedPantries, setSelectedPantries] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
@@ -40,7 +33,9 @@ const ApprovePantries: React.FC = () => {
try {
const data = await ApiClient.getAllPendingPantries();
setPantries(data);
+ setHasError(false);
} catch {
+ setHasError(true);
setAlertMessage('Error fetching pantries', AlertStatus.ERROR);
}
};
@@ -82,7 +77,6 @@ const ApprovePantries: React.FC = () => {
);
const itemsPerPage = 10;
- const totalPages = Math.ceil(filteredPantries.length / itemsPerPage);
const paginatedPantries = filteredPantries.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage,
@@ -125,7 +119,7 @@ const ApprovePantries: React.FC = () => {
timeout={6000}
/>
)}
- {pantries.length === 0 ? (
+ {!hasError && pantries.length === 0 ? (
{
There are no applications to review at this time
+ ) : hasError ? (
+
+
+
+
+
+ Unable to Load Applications
+
+
+ Something went wrong while loading applications. Please try again
+ later.
+
+
) : (
@@ -314,6 +333,15 @@ const ApprovePantries: React.FC = () => {
':applicationId',
pantry.pantryId.toString(),
)}
+ onClick={(event) => {
+ event.preventDefault();
+ navigate(
+ ROUTES.PANTRY_APPLICATION_DETAILS.replace(
+ ':applicationId',
+ pantry.pantryId.toString(),
+ ),
+ );
+ }}
>
View Details
@@ -323,55 +351,14 @@ const ApprovePantries: React.FC = () => {
- {totalPages > 1 && (
-
+ setCurrentPage(e.page)}
- >
-
-
-
-
-
- (
-
- {page.value}
-
- )}
- />
-
-
-
-
-
-
- )}
+ onPageChange={setCurrentPage}
+ />
+
)}
diff --git a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx
index 078d2a685..0af1a6626 100644
--- a/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx
+++ b/apps/frontend/src/containers/foodManufacturerDonationManagement.tsx
@@ -1,16 +1,6 @@
import ApiClient from '@api/apiClient';
-import {
- Box,
- Button,
- ButtonGroup,
- Flex,
- Heading,
- IconButton,
- Link,
- Pagination,
- Table,
-} from '@chakra-ui/react';
-import { ChevronRight, ChevronLeft, Mail } from 'lucide-react';
+import { Box, Button, Flex, Heading, Link, Table } from '@chakra-ui/react';
+import { Mail } from 'lucide-react';
import { capitalize, formatDate, DONATION_STATUS_COLORS } from '@utils/utils';
import {
AlertStatus,
@@ -29,6 +19,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
import { useAlert } from '../hooks/alert';
import FMDeleteDonationActionModal from '@components/forms/fmDeleteDonationModal';
import { ROUTES } from '../routes';
+import { PaginationControl } from '@components/pagination';
const MAX_PER_STATUS = 5;
@@ -385,8 +376,6 @@ const DonationStatusSection: React.FC = ({
onActionSelect,
showManufacturer,
}) => {
- const totalPages = Math.ceil(totalDonations / MAX_PER_STATUS);
-
const tableHeaderStyles = {
borderBottom: '1px solid',
borderColor: 'neutral.100',
@@ -559,65 +548,14 @@ const DonationStatusSection: React.FC = ({
- {totalPages > 1 && (
-
- onPageChange(e.page)}
- >
-
-
-
-
-
- (
-
- {page.value}
-
- )}
- />
-
-
-
-
-
-
-
- )}
+
+
+
>
)}
diff --git a/apps/frontend/src/containers/formRequests.tsx b/apps/frontend/src/containers/formRequests.tsx
index ead737050..81880c9b2 100644
--- a/apps/frontend/src/containers/formRequests.tsx
+++ b/apps/frontend/src/containers/formRequests.tsx
@@ -8,12 +8,8 @@ import {
useDisclosure,
Link,
Badge,
- Pagination,
- ButtonGroup,
- IconButton,
Flex,
} from '@chakra-ui/react';
-import { ChevronRight, ChevronLeft } from 'lucide-react';
import FoodRequestFormModal from '@components/forms/requestFormModal';
import { FoodRequestStatus, FoodRequestSummaryDto } from '../types/types';
import RequestDetailsModal from '@components/forms/requestDetailsModal';
@@ -25,6 +21,7 @@ import { useAlert } from '../hooks/alert';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { ROUTES } from '../routes';
import { AlertStatus } from '../types/types';
+import { PaginationControl } from '@components/pagination';
const FormRequests: React.FC = () => {
const [currentPage, setCurrentPage] = useState(1);
@@ -262,50 +259,12 @@ const FormRequests: React.FC = () => {
/>
)}
- setCurrentPage(page)}
- >
-
-
- setCurrentPage((prev) => Math.max(prev - 1, 1))}
- >
-
-
-
-
- (
- setCurrentPage(page.value)}
- >
- {page.value}
-
- )}
- />
-
-
-
- setCurrentPage((prev) =>
- Math.min(prev + 1, Math.ceil(requests.length / pageSize)),
- )
- }
- >
-
-
-
-
-
+ onPageChange={setCurrentPage}
+ />
);
diff --git a/apps/frontend/src/containers/homepage.tsx b/apps/frontend/src/containers/homepage.tsx
deleted file mode 100644
index 0e9e931c7..000000000
--- a/apps/frontend/src/containers/homepage.tsx
+++ /dev/null
@@ -1,245 +0,0 @@
-import React from 'react';
-import { Link as RouterLink } from 'react-router-dom';
-import { ROUTES } from '../routes';
-import {
- Box,
- Container,
- Heading,
- VStack,
- List,
- ListItem,
- Link,
- Text,
- Alert,
-} from '@chakra-ui/react';
-import { useAuthenticator } from '@aws-amplify/ui-react';
-
-const Homepage: React.FC = () => {
- const { authStatus } = useAuthenticator((context) => [context.authStatus]);
-
- return (
-
-
-
- Site Navigation
-
-
-
-
- Profile View
-
-
-
-
- Pantry View
-
-
-
-
- Dashboard
-
-
-
-
- Request Form
-
-
-
-
-
- Pantry Application
-
-
-
-
-
-
- Order Management
-
-
-
-
-
-
-
-
- Food Manufacturer View
-
-
-
-
-
- Donation Management
-
-
-
-
-
-
- Food Manufacturer Application
-
-
-
-
-
-
- Food Manufacturer Dashboard
-
-
-
-
-
-
-
-
- Volunteer View
-
-
-
-
-
- Dashboard
-
-
-
-
-
-
- Assigned Pantries
-
-
-
-
-
-
- Food Request Management
-
-
-
-
-
-
- Order Management
-
-
-
-
-
-
-
-
- Admin View
-
-
-
-
-
- Approve Pantries
-
-
-
-
-
-
- Approve Food Manufacturers
-
-
-
-
-
-
- User Management
-
-
-
-
-
-
- Donation Management
-
-
-
-
-
-
- Donation Statistics
-
-
-
-
-
-
- Order Management
-
-
-
-
-
-
- Pantry Management
-
-
-
-
-
- Dashboard
-
-
-
-
-
- Food Request Management
-
-
-
-
-
-
- {authStatus !== 'authenticated' && (
-
-
- Other Pages
-
-
-
-
- Login
-
-
-
-
- Sign Up
-
-
-
-
- )}
-
-
-
-
-
-
- Note: This is a temporary navigation page for
- development purposes.
-
-
-
-
-
-
-
- );
-};
-
-export default Homepage;
diff --git a/apps/frontend/src/containers/loginPage.tsx b/apps/frontend/src/containers/loginPage.tsx
index dfcd36602..5a37dc4b3 100644
--- a/apps/frontend/src/containers/loginPage.tsx
+++ b/apps/frontend/src/containers/loginPage.tsx
@@ -37,7 +37,7 @@ const LoginPage: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
- const from = location.state?.from?.pathname || ROUTES.HOME;
+ const from = location.state?.from?.pathname || ROUTES.PROFILE;
useEffect(() => {
if (authStatus === 'authenticated') {
diff --git a/apps/frontend/src/containers/pantryOrderManagement.tsx b/apps/frontend/src/containers/pantryOrderManagement.tsx
index 1efd6fc71..95daa2a82 100644
--- a/apps/frontend/src/containers/pantryOrderManagement.tsx
+++ b/apps/frontend/src/containers/pantryOrderManagement.tsx
@@ -1,21 +1,6 @@
import React, { useState, useEffect, useCallback } from 'react';
-import {
- Box,
- Button,
- Table,
- Heading,
- Pagination,
- IconButton,
- VStack,
- ButtonGroup,
-} from '@chakra-ui/react';
-import {
- ArrowDownUp,
- ChevronRight,
- ChevronLeft,
- Mail,
- CircleCheck,
-} from 'lucide-react';
+import { Box, Button, Table, Heading, VStack } from '@chakra-ui/react';
+import { ArrowDownUp, Mail, CircleCheck } from 'lucide-react';
import {
formatDate,
getInitials,
@@ -31,6 +16,7 @@ import { FloatingAlert } from '@components/floatingAlert';
import { useAlert } from '../hooks/alert';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { ROUTES } from '../routes';
+import { PaginationControl } from '@components/pagination';
type OrderWithColor = OrderSummary & { assigneeColor?: string };
const MAX_PER_STATUS = 5;
@@ -301,8 +287,6 @@ const OrderStatusSection: React.FC = ({
}) => {
const [isSortOpen, setIsSortOpen] = useState(false);
- const totalPages = Math.ceil(totalOrders / MAX_PER_STATUS);
-
const tableHeaderStyles = {
borderBottom: '1px solid',
borderColor: 'neutral.100',
@@ -630,65 +614,14 @@ const OrderStatusSection: React.FC = ({
- {totalPages > 1 && (
-
- onPageChange(e.page)}
- >
-
-
-
-
-
- (
-
- {page.value}
-
- )}
- />
-
-
-
-
-
-
-
- )}
+
+
+
>
)}
diff --git a/apps/frontend/src/containers/userManagement.tsx b/apps/frontend/src/containers/userManagement.tsx
index 104df7eac..c6034175f 100644
--- a/apps/frontend/src/containers/userManagement.tsx
+++ b/apps/frontend/src/containers/userManagement.tsx
@@ -10,19 +10,12 @@ import {
Box,
Badge,
InputGroup,
- Pagination,
- ButtonGroup,
IconButton,
Link,
Menu,
Portal,
} from '@chakra-ui/react';
-import {
- SearchIcon,
- ChevronRight,
- ChevronLeft,
- EllipsisVertical,
-} from 'lucide-react';
+import { SearchIcon, EllipsisVertical } from 'lucide-react';
import { AlertStatus, Role, User } from '../types/types';
import ApiClient from '@api/apiClient';
import NewVolunteerModal from '@components/forms/addNewVolunteerModal';
@@ -31,6 +24,7 @@ import ConfirmActionModal from '@components/forms/confirmActionModal';
import { FloatingAlert } from '@components/floatingAlert';
import { useAlert } from '../hooks/alert';
import { getInitials, USER_ICON_COLORS } from '@utils/utils';
+import { PaginationControl } from '@components/pagination';
const VolunteerManagement: React.FC = () => {
const navigate = useNavigate();
@@ -219,7 +213,7 @@ const VolunteerManagement: React.FC = () => {
{paginatedVolunteers?.map((volunteer) => (
-
+
{
{getInitials(volunteer.firstName, volunteer.lastName)}
{volunteer.firstName} {volunteer.lastName}
+ {volunteer.role === Role.ADMIN && (
+
+ Admin
+
+ )}
@@ -322,57 +333,12 @@ const VolunteerManagement: React.FC = () => {
- setCurrentPage(page)}
- >
-
-
-
- setCurrentPage((prev) => Math.max(prev - 1, 1))
- }
- >
-
-
-
-
- (
- setCurrentPage(page.value)}
- >
- {page.value}
-
- )}
- />
-
-
-
- setCurrentPage((prev) =>
- Math.min(
- prev + 1,
- Math.ceil(filteredVolunteers.length / pageSize),
- ),
- )
- }
- >
-
-
-
-
-
+ onPageChange={setCurrentPage}
+ />
diff --git a/apps/frontend/src/containers/volunteerOrderManagement.tsx b/apps/frontend/src/containers/volunteerOrderManagement.tsx
index ff193db2a..f396a2a2b 100644
--- a/apps/frontend/src/containers/volunteerOrderManagement.tsx
+++ b/apps/frontend/src/containers/volunteerOrderManagement.tsx
@@ -4,24 +4,13 @@ import {
Button,
Table,
Heading,
- Pagination,
- IconButton,
VStack,
- ButtonGroup,
Checkbox,
Input,
Link,
Spinner,
} from '@chakra-ui/react';
-import {
- ArrowDownUp,
- ChevronRight,
- ChevronLeft,
- Funnel,
- Mail,
- CircleCheck,
- Search,
-} from 'lucide-react';
+import { ArrowDownUp, Funnel, Mail, CircleCheck, Search } from 'lucide-react';
import {
formatDate,
getInitials,
@@ -45,6 +34,7 @@ import { FloatingAlert } from '@components/floatingAlert';
import { useAlert } from '../hooks/alert';
import { useNavigate, useSearchParams } from 'react-router-dom';
import { ROUTES } from '../routes';
+import { PaginationControl } from '@components/pagination';
type VolunteerOrderWithColor = VolunteerOrder & { assigneeColor?: string };
@@ -447,7 +437,6 @@ const OrderStatusSection: React.FC = ({
const [isSortOpen, setIsSortOpen] = useState(false);
const MAX_PER_STATUS = 5;
- const totalPages = Math.ceil(totalOrders / MAX_PER_STATUS);
const handleFilterChange = (pantry: string, checked: boolean) => {
const newSelected = checked
@@ -921,66 +910,14 @@ const OrderStatusSection: React.FC = ({
- {totalPages > 1 && (
-
- onPageChange(e.page)}
- >
-
-
-
-
-
- (
-
- {page.value}
-
- )}
- />
-
-
-
-
-
-
-
- )}
+
+
+
>
)}
>