DEV Community

Cover image for Day 95 of #100DaysOfCode — DevCollab: UI Polish, Error Handling, and Loading States
M Saad Ahmad
M Saad Ahmad

Posted on

Day 95 of #100DaysOfCode — DevCollab: UI Polish, Error Handling, and Loading States

All the pages are built. Everything works. But "works" and "feels good to use" are two different things. Today was about closing that gap, consistent styling, proper error handling, skeleton loading states, empty states that guide users, and toast notifications that replace raw alerts. No new features. Just making what exists feel like a real product.


Why Polish Day Matters

It's tempting to skip this day and go straight to deployment. The app functions, the data flows correctly, and the permissions are enforced. What's the point of spending a whole day on loading spinners and empty state messages?

The point is that a portfolio project isn't just evaluated on whether it works; it's evaluated on whether it feels like something a real team would ship. Raw API error text appearing in the UI, pages that flash blank while loading, empty tables with no explanation, these are the things that make a project feel unfinished, even when the underlying code is solid. One focused polish day removes all of those.


The Toast Notification System

The single biggest improvement today was replacing scattered alert() calls and inline error message strings with a unified toast notification system.

Before today, error handling was inconsistent. Some pages showed errors in a red div above the form. Some pages used window.alert(). Some pages silently failed. A user accepting a collaboration request on the dashboard had no feedback confirming it worked; the button just changed state with no message.

The toast system fixes all of this. It's a context-based notification system: a ToastContext that any component can call to show a success, error, or info notification. The toast appears in the bottom-right corner of the screen, stays visible for three seconds, and then fades out. Multiple toasts can stack. Each has an appropriate color: green for success, red for error, and blue for info.

Every action that previously had inconsistent feedback now calls the toast system. Project created; green toast. Request sent; green toast. Request accepted; green toast. API call failed; red toast with a readable message extracted from the Django error response. The user always knows what happened.


Skeleton Loading States

The previous loading implementation was a centered spinner on every page. It worked, but it was jarring; the page would be blank, then a spinner, then suddenly full of content. The content shift felt abrupt.

Skeleton screens solve this by showing placeholder shapes in the layout that the content will occupy. The browse projects page now shows three skeleton project cards during load, grey rectangles in the same grid as real cards, with the same card dimensions, borders, and rounded corners. When the real data arrives, the skeletons are replaced. The layout doesn't shift because the placeholders already occupy the same space.

The dashboard shows skeleton rows for the projects section and skeleton request cards for the incoming requests section. The profile page shows a skeleton avatar circle, skeleton text lines for the bio and skills, and skeleton project cards below. Each skeleton matches the shape of its real content, so the transition from loading to loaded feels smooth rather than abrupt.


Empty States with Guidance

Every page that can legitimately have zero items now has a proper empty state. The key insight about empty states is that they're not just informational, they're navigational. A user landing on an empty page needs to know what to do next, not just that nothing is there yet.

The browse projects page, when no projects exist, or no results match the search, shows a message explaining the situation and either a "Clear filters" link or a "Post the first project" link, depending on whether filters are active.

The dashboard's projects section, for a new user who hasn't posted anything yet, shows an illustration space, the message "You haven't posted any projects yet", and a prominent "Post Your First Project" button. The dashboard's requests section, when no requests have come in, shows "No collaboration requests yet" with a softer message explaining that requests will appear here when developers apply to their projects.

The sent requests page for a new user shows "You haven't applied to any projects yet" with a link to browse projects.

The profile page, when viewed as a logged-in user who hasn't set up their profile, shows a subtle prompt to complete the profile: "Your profile is looking a bit empty. Add your skills and bio so other developers know what you can bring to a project."


Consistent Styling Pass

Going through every page on the same day revealed a lot of small inconsistencies that I hadn't noticed when building them on different days. The create project page used slightly different button styling than the edit project page. The browse page cards had different padding than the dashboard project rows. Some pages had a max-width container, some didn't.

I went through each page and applied a consistent set of Tailwind patterns. All cards have the same padding, border, shadow, and hover transition. All primary buttons are the same blue with the same rounded corners. All headings at the same level use the same font size and weight. All form fields look identical across all forms, with the same border, same focus ring, and same error state styling.

This kind of pass is only possible efficiently when all the pages exist at the same time. Building them one at a time means you can't see the inconsistencies until you view the whole app together.


Error Handling Improvements

Raw API error responses are not user-friendly. Django returns errors in formats like {"title": ["This field may not be blank."]} or {"detail": "Authentication credentials were not provided."}. Before today, these were sometimes shown directly to users.

I wrote a small helper function: parseApiError, that takes an error response from the API and returns a readable string. It handles the most common Django error formats: field-specific errors as an object, a detail string, a non-field errors array, and a plain error string. The output is always a clean sentence that the user can read.

Every catch block in every service call now runs the error through parseApiError before showing it in a toast or form error state. No more raw JSON visible anywhere in the UI.


Form Feedback Improvements

Three specific improvements to forms across the app.

First, submit buttons are consistently disabled while a form is submitting. Before today, some forms disabled their button during submission, and some didn't. Consistent behavior means users can't accidentally double-submit a form on a slow connection.

Second, form field validation errors are visually clearer. The red error text is now positioned immediately below the relevant field with a red border on the input, not just red text somewhere on the page.

Third, the edit profile and edit project forms now show a subtle "No changes made" message if the user submits the form without changing anything. This is detected by comparing the current form values to the initial values. If nothing changed, there's no point in making an API call.


Navbar Active State

A small detail that makes navigation feel more intentional: the current page's link in the Navbar is highlighted. The usePathname hook from Next.js returns the current URL path. The Navbar compares each link's target path to the current pathname and applies an active style, a slightly different text color, and a subtle underline to the matching link.


Where Things Stand After Day 95

The app looks consistent. Errors are friendly. Loading states are smooth. Empty states guide users toward action. Forms give clear feedback. The entire UI has been reviewed in one session, and inconsistencies are resolved.

Thanks for reading. Feel free to share your thoughts!

Top comments (0)