emoney-profit-tracker is a small operational app for a reseller workflow. It tracks purchased products, completed sales, profit trends over time, and a lightweight "quests" system for recurring tasks. The problem it solves is familiar: once buying, listing, shipping, and bookkeeping are spread across different tools, it becomes hard to answer basic questions like "what is actually making money?" and "what still needs attention?"
This codebase answers that with a single Next.js 15 App Router application. It is not trying to be a heavily server-rendered product. Most of the interesting work happens on the client: auth checks, data fetching, table filtering, form state, and mutation flows. That makes the app easy to reason about for dashboard-style work, even if it leaves some server-side capabilities unused.
Architecture and the shape of the app
The project is organised around App Router route groups plus feature-specific client components. app/layout.tsx defines the global shell, metadata, Providers, and the Toaster. The authenticated area lives under app/(aunthenticated), with app/(aunthenticated)/layout.tsx layering TopNav and MobileNavigation around the page body. Individual screens mostly render thin route files that hand off to client components like DashboardClient, ProductsClient, and QuestsClient.
That split works well here. Route files stay small, while screen logic sits close to the UI that owns it.
The service layer is intentionally thin. Files like services/product.service.ts, services/sales.service.ts, and services/quests.service.ts mostly expose small functions that call a shared Axios client from services/_http.service.ts. That shared client sets a fixed baseURL, enables withCredentials, and centralises 401 handling and network-error toasts:
api.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
if (typeof window !== "undefined") {
const currentPath = window.location.pathname
sessionStorage.setItem("callbackPath", currentPath)
window.location.href = "/login"
}
}
return Promise.reject(error)
}
)
That keeps auth-expiry handling out of page components.
Data flow and state management
Remote state is handled with React Query, wrapped by lib/hooks/use-query-resource.ts. The wrappers are simple but useful: useGetResource standardises query execution and callback handling, while useModifyResource and useDeleteResource invalidate a provided query key after mutation.
return useMutation({
...mutationOptions,
mutationFn: fn,
onSuccess: (data) => {
if (onSuccess) onSuccess(data)
queryClient.invalidateQueries({ queryKey: key })
},
})
That pattern shows up almost everywhere. ProductsClient fetches a list with useGetResource({ fn: getProducts, key: ["products"] }). The product table's delete action uses useModifyResource with the same key so the list refreshes without manual cache bookkeeping.
Local UI state is kept local. Search input, selected tabs, dialog visibility, selected files, and transient submission state all live in component state rather than being pushed into a global store. Forms use React Hook Form and Zod, which is the right fit here: field registration stays cheap, validation is explicit, and submit handlers can sanitize outgoing payloads before mutation.
Authentication is also managed on the client in components/providers/auth-context.tsx. AuthProvider stores the current user and an authStatus, while AuthGuard calls getUser() once on mount, redirects unauthenticated users to /login, and preserves the intended path in sessionStorage. The login button in app/login/components/login-button.tsx builds a Discord OAuth URL that uses that saved path as state.
This is centralized and easy to follow, but it is also fully client-side. Protected routes are redirected after hydration rather than being rejected earlier by middleware or server components.
Interesting implementation details
One of the better pieces of UI logic is in app/(aunthenticated)/sales/add-sale/_components/add-sale-form.tsx. The form does more than collect fields. It debounces product lookup with useDebounce, loads matching products through productsLookup, and adjusts validation based on the selected product's stock level:
const createDynamicSchema = (maxQuantity?: number) => {
return z.object({
quantity: maxQuantity
? z.number().min(1).max(maxQuantity, `Quantity cannot exceed ${maxQuantity}`)
: z.number().min(1),
})
}
When a product is selected, the form also pre-fills sale_price from purchase_price and clamps quantity if it exceeds stock. That is a good example of business rules living in the form layer.
Another strong detail is the quest proof submission flow in app/(aunthenticated)/quests/_components/quests-proof-submission.tsx. The same form renders as a Dialog on desktop and a Drawer on mobile using useIsMobile(450). The upload side is handled by components/shared/file-uploader.tsx, which uses react-dropzone, generates preview URLs with URL.createObjectURL, and passes real File objects into a FormData payload.
The shared table in components/shared/data-table/index.tsx is another important building block. It standardises sorting, filtering, pagination, loading skeletons, empty states, and "add new" actions on top of TanStack Table. That keeps SalesClient and ProductsClient small.
There are also a few rough edges worth calling out, honestly. The QueryClient is instantiated inside the Providers component on every render:
const Providers: React.FC<React.PropsWithChildren> = ({ children }) => {
const queryClient = new QueryClient()
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
That risks resetting cache state if the provider re-renders. It would be safer to memoize it or create it once outside the component.
There are also some endpoint mismatches that look like copy-paste drift. In the sales edit flow, app/(aunthenticated)/sales/edit-sale/[slug]/page.tsx calls getSingleProduct, and the sale form's edit path uses updateProduct. Likewise, some delete actions for sales and recent transactions call deleteProduct.
The admin area is another revealing detail. app/(aunthenticated)/control/admin/_components/admin-client.tsx is visually fleshed out, but its metrics are currently static. Compared with the sales, products, and quests flows, it reads more like a dashboard stub than a finished screen.
Performance, security, and accessibility
Performance work here is practical rather than exotic. The app debounces product lookup, uses React Query caching and invalidation to avoid redundant fetch bookkeeping, and leans on next/image through components/shared/image-loader.tsx for product imagery.
Security is mostly about session handling. Axios requests are sent with credentials, 401s are intercepted centrally, and the login flow preserves the callback path through OAuth state. What is missing is stronger server-side enforcement in the frontend layer; because auth is client-guarded, a lot of route protection depends on hydration-time redirects rather than server rejection.
Accessibility is mixed but generally thoughtful. Many controls come from Radix-based UI primitives, which is a strong baseline. The app uses labels, tooltips, dialog primitives, and sr-only text in menu triggers. A few custom pieces would benefit from more attention, especially image fallbacks and drag-and-drop affordances.
What I would change next
If I were continuing this codebase, I would keep the general architecture but tighten the boundaries. First, I would stabilize the React Query provider and formalize query keys. Second, I would separate product and sale service contracts more aggressively so endpoint mix-ups become harder to write. Third, I would move at least part of auth enforcement into middleware or server-side route handling.
The larger lesson is that this app gets a lot of value from a modest amount of structure. Thin services, reusable query hooks, shared table infrastructure, and form-driven business rules go a long way in an internal dashboard. The tradeoff is that once those abstractions exist, they need naming discipline and a few guardrails. emoney-profit-tracker is a good example of a pragmatic frontend that solves a real operational problem while making its next engineering steps fairly obvious.

Top comments (0)