-
Notifications
You must be signed in to change notification settings - Fork 0
[SSF-206] Add empty state components for dashboard #181
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
base: main
Are you sure you want to change the base?
Changes from all commits
6e45629
575b115
350bbf6
fd37812
0c09bd9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import React from 'react'; | ||
| import { Box, Button, Text } from '@chakra-ui/react'; | ||
| import { CircleCheck } from 'lucide-react'; | ||
| import { useNavigate } from 'react-router-dom'; | ||
|
|
||
| interface PageEmptyStateProps { | ||
| entity?: string; | ||
| subtitle?: string; | ||
| primaryButtonText: string; | ||
| primaryButtonLink: string; | ||
| secondaryButtonText: string; | ||
| secondaryButtonLink: string; | ||
| } | ||
|
|
||
| const PageEmptyState: React.FC<PageEmptyStateProps> = ({ | ||
| entity, | ||
| subtitle, | ||
| primaryButtonText, | ||
| primaryButtonLink, | ||
| secondaryButtonText, | ||
| secondaryButtonLink, | ||
| }) => { | ||
| const navigate = useNavigate(); | ||
| const message = subtitle ?? `You have no ${entity} at this time`; | ||
|
|
||
| return ( | ||
| <Box | ||
| display="flex" | ||
| flexDirection="column" | ||
| alignItems="center" | ||
| justifyContent="center" | ||
| textAlign="center" | ||
| py={10} | ||
| gap={2} | ||
| > | ||
| <Box mb={2}> | ||
| <CircleCheck size={24} color="var(--chakra-colors-neutral-800)" /> | ||
| </Box> | ||
| <Text fontWeight="600" textStyle="p" color="neutral.800"> | ||
| Nothing to see here! | ||
| </Text> | ||
| <Text textStyle="p2" color="neutral.700" fontWeight="400"> | ||
| {message} | ||
| </Text> | ||
| <Box display="flex" gap={3} mt={4}> | ||
| <Button | ||
| size="sm" | ||
| bg="neutral.700" | ||
|
Yurika-Kan marked this conversation as resolved.
|
||
| color="white" | ||
| _hover={{ bg: 'neutral.800' }} | ||
| onClick={() => navigate(primaryButtonLink)} | ||
| > | ||
| {primaryButtonText} | ||
| </Button> | ||
| <Button | ||
| size="sm" | ||
| variant="outline" | ||
| borderColor="neutral.200" | ||
| color="neutral.700" | ||
| _hover={{ bg: 'neutral.50' }} | ||
| onClick={() => navigate(secondaryButtonLink)} | ||
| > | ||
| {secondaryButtonText} | ||
| </Button> | ||
| </Box> | ||
| </Box> | ||
| ); | ||
| }; | ||
|
|
||
| export default PageEmptyState; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| import { Box, Text } from '@chakra-ui/react'; | ||
|
|
||
| interface EmptyStateProps { | ||
| entity?: string; | ||
| subtitle?: string; | ||
| } | ||
|
|
||
| const SectionEmptyState: React.FC<EmptyStateProps> = ({ entity, subtitle }) => { | ||
| const message = subtitle ?? `You have no ${entity} at this time`; | ||
| return ( | ||
| <Box | ||
| display="flex" | ||
| flexDirection="column" | ||
| alignItems="center" | ||
| justifyContent="center" | ||
| textAlign="center" | ||
| py={10} | ||
| gap={2} | ||
| > | ||
| <Text fontWeight="600" textStyle="p" color="neutral.800"> | ||
| Nothing to see here! | ||
| </Text> | ||
| <Text textStyle="p2" color="neutral.700" fontWeight="400"> | ||
| {message} | ||
| </Text> | ||
| </Box> | ||
| ); | ||
| }; | ||
|
|
||
| export default SectionEmptyState; |
|
Yurika-Kan marked this conversation as resolved.
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16,6 +16,8 @@ import { useAlert } from '../hooks/alert'; | |
| import { FloatingAlert } from '@components/floatingAlert'; | ||
| import { useNavigate } from 'react-router-dom'; | ||
| import { ROUTES } from '../routes'; | ||
| import SectionEmptyState from '@components/sectionEmptyState'; | ||
| import PageEmptyState from '@components/pageEmptyState'; | ||
|
|
||
| const AdminDashboard: React.FC = () => { | ||
| const navigate = useNavigate(); | ||
|
|
@@ -84,6 +86,11 @@ const AdminDashboard: React.FC = () => { | |
| fetchPendingApplications(); | ||
| }, [setAlertMessage]); | ||
|
|
||
| const isPageEmpty = | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. isPageEmpty is computed from array lengths that start empty, so on first render when the page is still waiting for the data to be fetched, it shows empty state component instead. this is misleading to the user as they will think there are no ex. pantry applications to review vs the actual case that the data was being fetched. i suggest implementing a loading flag to differentiate loading vs no data states. for example, this pattern in const [loading, setLoading] = useState(true); lmk your thoughts on this~ if you test the pages too you can see what im referring to: flashes of the empty state component before data loads. and can you apply that to the other three dashboards~ |
||
| pendingApplications.length === 0 && | ||
| recentOrders.length === 0 && | ||
| recentDonations.length === 0; | ||
|
|
||
| return ( | ||
| <Box p={12}> | ||
| {alertState && ( | ||
|
|
@@ -98,84 +105,133 @@ const AdminDashboard: React.FC = () => { | |
| Welcome, {currentUser?.firstName} {currentUser?.lastName} | ||
| </Heading> | ||
|
|
||
| <Text textStyle="p" color="gray.light" fontWeight={600} mb={4}> | ||
| Pending Actions | ||
| </Text> | ||
| <Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={4} mb={16}> | ||
| {pendingApplications.map((application) => ( | ||
| <DashboardCard | ||
| type={DashboardCardType.ACTION} | ||
| title={application.name} | ||
| date={application.dateApplied} | ||
| key={application.id} | ||
| linkText="View Application Details" | ||
| badge={{ | ||
| label: | ||
| application.type === 'pantry' ? 'Pantry' : 'Food Manufacturer', | ||
| bg: 'neutral.100', | ||
| color: 'neutral.600', | ||
| }} | ||
| onLinkClick={() => { | ||
| navigate( | ||
| application.type === 'pantry' | ||
| ? ROUTES.PANTRY_MANAGEMENT_DETAILS.replace( | ||
| ':pantryId', | ||
| application.id.toString(), | ||
| ) | ||
| : ROUTES.FOOD_MANUFACTURER_APPLICATION_DETAILS.replace( | ||
| ':applicationId', | ||
| application.id.toString(), | ||
| ), | ||
| ); | ||
| }} | ||
| /> | ||
| ))} | ||
| </Box> | ||
| {isPageEmpty ? ( | ||
| <PageEmptyState | ||
| entity="orders or applications to review" | ||
| primaryButtonText="View Pantries" | ||
| primaryButtonLink={ROUTES.PANTRY_MANAGEMENT} | ||
| secondaryButtonText="View Donations" | ||
| secondaryButtonLink={ROUTES.ADMIN_DONATION} | ||
| /> | ||
| ) : ( | ||
| <> | ||
| <Text textStyle="p" color="gray.light" fontWeight={600} mb={4}> | ||
| Pending Actions | ||
| </Text> | ||
| {pendingApplications.length === 0 ? ( | ||
| <Box mb={16}> | ||
| <SectionEmptyState entity="pending applications" /> | ||
| </Box> | ||
| ) : ( | ||
| <Box | ||
| display="grid" | ||
| gridTemplateColumns="repeat(2, 1fr)" | ||
| gap={4} | ||
| mb={16} | ||
| > | ||
| {pendingApplications.map((application) => ( | ||
| <DashboardCard | ||
| type={DashboardCardType.ACTION} | ||
| title={application.name} | ||
| date={application.dateApplied} | ||
| key={application.id} | ||
| linkText="View Application Details" | ||
| badge={{ | ||
| label: | ||
| application.type === 'pantry' | ||
| ? 'Pantry' | ||
| : 'Food Manufacturer', | ||
| bg: 'neutral.100', | ||
| color: 'neutral.600', | ||
| }} | ||
| onLinkClick={() => { | ||
| navigate( | ||
| application.type === 'pantry' | ||
| ? ROUTES.PANTRY_MANAGEMENT_DETAILS.replace( | ||
| ':pantryId', | ||
| application.id.toString(), | ||
| ) | ||
| : ROUTES.FOOD_MANUFACTURER_APPLICATION_DETAILS.replace( | ||
| ':applicationId', | ||
| application.id.toString(), | ||
| ), | ||
| ); | ||
| }} | ||
| /> | ||
| ))} | ||
| </Box> | ||
| )} | ||
|
|
||
| <Text textStyle="p" color="gray.light" fontWeight={600} mb={4}> | ||
| Recent Orders | ||
| </Text> | ||
| <Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={4} mb={16}> | ||
| {recentOrders.map((order) => ( | ||
| <DashboardCard | ||
| key={order.orderId} | ||
| type={DashboardCardType.ORDER} | ||
| title={`Order #${order.orderId}`} | ||
| date={order.createdAt} | ||
| subtitle={order.request.pantry.pantryName} | ||
| linkText="View Order Details" | ||
| badge={ORDER_STATUS_BADGE[order.status]} | ||
| assignee={{ | ||
| id: order.assignee.id, | ||
| firstName: order.assignee.firstName, | ||
| lastName: order.assignee.lastName, | ||
| }} | ||
| onLinkClick={() => | ||
| navigate(`/admin-order-management?orderId=${order.orderId}`) | ||
| } | ||
| /> | ||
| ))} | ||
| </Box> | ||
| <Text textStyle="p" color="gray.light" fontWeight={600} mb={4}> | ||
| Recent Orders | ||
| </Text> | ||
| {recentOrders.length === 0 ? ( | ||
| <Box mb={16}> | ||
| <SectionEmptyState entity="recent orders" /> | ||
| </Box> | ||
| ) : ( | ||
| <Box | ||
| display="grid" | ||
| gridTemplateColumns="repeat(2, 1fr)" | ||
| gap={4} | ||
| mb={16} | ||
| > | ||
| {recentOrders.map((order) => ( | ||
| <DashboardCard | ||
| key={order.orderId} | ||
| type={DashboardCardType.ORDER} | ||
| title={`Order #${order.orderId}`} | ||
| date={order.createdAt} | ||
| subtitle={order.request.pantry.pantryName} | ||
| linkText="View Order Details" | ||
| badge={ORDER_STATUS_BADGE[order.status]} | ||
| assignee={{ | ||
| id: order.assignee.id, | ||
| firstName: order.assignee.firstName, | ||
| lastName: order.assignee.lastName, | ||
| }} | ||
| onLinkClick={() => | ||
| navigate(`/admin-order-management?orderId=${order.orderId}`) | ||
| } | ||
| /> | ||
| ))} | ||
| </Box> | ||
| )} | ||
|
|
||
| <Text textStyle="p" color="gray.light" fontWeight={600} mb={4}> | ||
| Recent Donations | ||
| </Text> | ||
| <Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={4} mb={16}> | ||
| {recentDonations.map((donation) => ( | ||
| <DashboardCard | ||
| key={donation.donationId} | ||
| type={DashboardCardType.RECENT_DONATION} | ||
| title={`Donation #${donation.donationId}`} | ||
| date={donation.dateDonated} | ||
| subtitle={donation.foodManufacturer?.foodManufacturerName} | ||
| linkText="View Donation Details" | ||
| badge={DONATION_STATUS_BADGE[donation.status]} | ||
| onLinkClick={() => | ||
| navigate(`/admin-donation?donationId=${donation.donationId}`) | ||
| } | ||
| /> | ||
| ))} | ||
| </Box> | ||
| <Text textStyle="p" color="gray.light" fontWeight={600} mb={4}> | ||
| Recent Donations | ||
| </Text> | ||
| {recentDonations.length === 0 ? ( | ||
| <Box mb={16}> | ||
| <SectionEmptyState entity="recent donations" /> | ||
| </Box> | ||
| ) : ( | ||
| <Box | ||
| display="grid" | ||
| gridTemplateColumns="repeat(2, 1fr)" | ||
| gap={4} | ||
| mb={16} | ||
| > | ||
| {recentDonations.map((donation) => ( | ||
| <DashboardCard | ||
| key={donation.donationId} | ||
| type={DashboardCardType.RECENT_DONATION} | ||
| title={`Donation #${donation.donationId}`} | ||
| date={donation.dateDonated} | ||
| subtitle={donation.foodManufacturer?.foodManufacturerName} | ||
| linkText="View Donation Details" | ||
| badge={DONATION_STATUS_BADGE[donation.status]} | ||
| onLinkClick={() => | ||
| navigate( | ||
| `/admin-donation?donationId=${donation.donationId}`, | ||
| ) | ||
| } | ||
| /> | ||
| ))} | ||
| </Box> | ||
| )} | ||
| </> | ||
| )} | ||
| </Box> | ||
| ); | ||
| }; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
our files are named in camelCase~ could you rename the empty state files accordingly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
like pageEmptyState.tsx @jxuistrying