Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion client/.env
Original file line number Diff line number Diff line change
@@ -1 +0,0 @@
VITE_API_URL='https://subly-backend-iuej.onrender.com'
9 changes: 7 additions & 2 deletions client/src/components/Navbar.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NavLink } from "react-router"
import { NavLink, useRouteLoaderData } from "react-router"
import { navLinks } from "../lib/var"

function BottomNavLink({ item }) {
Expand Down Expand Up @@ -36,10 +36,15 @@ function BottomNavLink({ item }) {
}

export function Navbar() {
const data = useRouteLoaderData("dashboard");
const visibleLinks = navLinks.filter(
(item) => !item.adminOnly || data?.user?.role === "ADMIN",
);

return (
<nav className="fixed bottom-0 left-0 right-0 z-50 border-t border-subly-border bg-subly-card/95 px-2 py-2 shadow-[0_-8px_30px_rgba(8,17,31,0.08)] backdrop-blur lg:hidden">
<div className="mx-auto flex max-w-md items-center gap-1">
{navLinks.map((item) => (
{visibleLinks.map((item) => (
<BottomNavLink key={item.path} item={item} />
))}
</div>
Expand Down
10 changes: 7 additions & 3 deletions client/src/components/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { NavLink} from "react-router";
import { NavLink, useRouteLoaderData } from "react-router";
import { navLinks } from "../lib/var";
import SublyLogo from "./SublyLogo";

Expand Down Expand Up @@ -40,14 +40,19 @@ function SidebarLink({ item }) {
);
}
export function Sidebar() {
const data = useRouteLoaderData("dashboard");
const visibleLinks = navLinks.filter(
(item) => !item.adminOnly || data?.user?.role === "ADMIN",
);

return (
<aside className="fixed left-0 top-0 hidden h-screen w-72 border-r border-subly-border bg-subly-card px-5 py-6 lg:block">
<div className="mb-10">
<SublyLogo />
</div>

<nav className="space-y-2">
{navLinks.map((item) => (
{visibleLinks.map((item) => (
<SidebarLink key={item.path} item={item} />
))}
</nav>
Expand All @@ -64,4 +69,3 @@ export function Sidebar() {
</aside>
);
}

22 changes: 16 additions & 6 deletions client/src/components/dashboard/Chart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,11 @@ export function SpendingOverviewChart({ chartData, baseCurrency }) {
}

export function SpendingByCategoryChart({ pieChart, baseCurrency }) {
const categoryTotal = Array.isArray(pieChart?.categoryTotal)
? pieChart.categoryTotal
: [];
const grandTotal = pieChart?.grandTotal ?? 0;

return (
<Card className="rounded-3xl border-subly-border bg-subly-card shadow-sm">
<CardHeader>
Expand Down Expand Up @@ -220,15 +225,15 @@ export function SpendingByCategoryChart({ pieChart, baseCurrency }) {
/>

<Pie
data={pieChart.categoryTotal}
data={categoryTotal}
dataKey="amount"
nameKey="category"
innerRadius={65}
outerRadius={92}
paddingAngle={4}
strokeWidth={0}
>
{pieChart?.categoryTotal.map((item) => (
{categoryTotal.map((item) => (
<Cell
key={item.category}
fill={chartConfig[item.id]?.color || "#888888"}
Expand All @@ -243,14 +248,14 @@ export function SpendingByCategoryChart({ pieChart, baseCurrency }) {
Total
</p>
<p className="text-lg font-bold text-subly-text-primary">
{formatMoney(pieChart?.grandTotal, baseCurrency)}
{formatMoney(grandTotal, baseCurrency)}
</p>
</div>
</div>

<div className="mt-5 space-y-3">
{pieChart &&
pieChart?.categoryTotal.map((item) => {
{categoryTotal.length > 0 ? (
categoryTotal.map((item) => {
return (
<div
key={item.category}
Expand All @@ -277,7 +282,12 @@ export function SpendingByCategoryChart({ pieChart, baseCurrency }) {
</div>
</div>
);
})}
})
) : (
<p className="rounded-xl border border-dashed border-subly-border px-4 py-6 text-center text-sm text-subly-text-secondary">
No category spending yet.
</p>
)}
</div>
</CardContent>
</Card>
Expand Down
1 change: 0 additions & 1 deletion client/src/components/ui/alert-dialog.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as React from "react"
import { AlertDialog as AlertDialogPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/ui/badge.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as React from "react"
/* eslint-disable react-refresh/only-export-components */
import { cva } from "class-variance-authority";
import { Slot } from "radix-ui"

Expand Down
1 change: 0 additions & 1 deletion client/src/components/ui/breadcrumb.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as React from "react"
import { Slot } from "radix-ui"

import { cn } from "@/lib/utils"
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/ui/button.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import * as React from "react"
/* eslint-disable react-refresh/only-export-components */
import { cva } from "class-variance-authority";
import { Slot } from "radix-ui"

Expand Down
2 changes: 0 additions & 2 deletions client/src/components/ui/card.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as React from "react"

import { cn } from "@/lib/utils"

function Card({
Expand Down
2 changes: 0 additions & 2 deletions client/src/components/ui/input.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as React from "react"

import { cn } from "@/lib/utils"

function Input({
Expand Down
1 change: 0 additions & 1 deletion client/src/components/ui/label.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as React from "react"
import { Label as LabelPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"
Expand Down
2 changes: 0 additions & 2 deletions client/src/components/ui/pagination.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as React from "react"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { ChevronLeftIcon, ChevronRightIcon, MoreHorizontalIcon } from "lucide-react"
Expand Down
1 change: 0 additions & 1 deletion client/src/components/ui/select.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as React from "react"
import { Select as SelectPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"
Expand Down
1 change: 0 additions & 1 deletion client/src/components/ui/separator.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client"

import * as React from "react"
import { Separator as SeparatorPrimitive } from "radix-ui"

import { cn } from "@/lib/utils"
Expand Down
2 changes: 0 additions & 2 deletions client/src/components/ui/table.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import * as React from "react"

import { cn } from "@/lib/utils"

function Table({
Expand Down
16 changes: 5 additions & 11 deletions client/src/lib/action.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { redirect } from "react-router";
import { RegisterSchemas, LoginSchemas } from "./zodType";
import { getApiBaseUrl } from "./utils";


export const register = async ({ request }) => {
const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:3000";
const API_BASE_URL = getApiBaseUrl();
try {
const formData = await request.formData();

const result = Object.fromEntries(formData);
const confirmPassword = result.confirmPassword;

console.log(result);

const parsedData = RegisterSchemas.safeParse(result);

if (!parsedData.success) {
Expand All @@ -31,8 +30,6 @@ export const register = async ({ request }) => {
message: "Invalid form input",
};
}
console.log(confirmPassword);

const response = await fetch(`${API_BASE_URL}/api/auth/register`, {
method: "POST",
headers: {
Expand All @@ -53,9 +50,7 @@ export const register = async ({ request }) => {
};
}

console.log(data);

return redirect("/auth/login");
return redirect("/dashboard");
} catch (err) {
console.error(err);

Expand All @@ -66,7 +61,7 @@ export const register = async ({ request }) => {
};

export const login = async ({ request }) => {
const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:3000";
const API_BASE_URL = getApiBaseUrl();
try {
const formData = await request.formData();

Expand Down Expand Up @@ -95,7 +90,6 @@ export const login = async ({ request }) => {
message: data.message || "Login failed",
};
}
console.log(data);
return redirect("/dashboard");
} catch (err) {
console.error(err);
Expand All @@ -104,4 +98,4 @@ export const login = async ({ request }) => {
message: "Something went wrong",
};
}
};
};
17 changes: 15 additions & 2 deletions client/src/lib/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,24 @@ export async function dashboardLoader() {
return await response.json();
} catch (err) {
console.error("Error fetching user data:", err);
redirect("/auth/login"); // Redirect to login page if unauthorized or on error
return redirect("/auth/login");
}
Comment on lines 10 to 13

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Stop redirecting to /auth/login for non-auth failures.

At Line 12, every caught error redirects to login. Since fetchWithAuth throws on any non-OK status, transient backend failures (e.g. /api/me 5xx) are treated as auth failures and force incorrect redirects.

💡 Suggested fix
 export async function dashboardLoader() {
   try {
     const response = await fetchWithAuth("/api/me");
     return await response.json();
   } catch (err) {
     console.error("Error fetching user data:", err);
-    return redirect("/auth/login");
+    if (err?.status === 401 || err?.status === 403) {
+      return redirect("/auth/login");
+    }
+    throw err;
    }
 }

Also make sure fetchWithAuth attaches status to thrown errors so this branch is reliable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/lib/loader.js` around lines 10 - 13, The catch block in the loader
redirects to `/auth/login` for all errors regardless of type, but this
incorrectly treats transient backend failures (like 5xx errors from `/api/me`)
as auth failures. Modify the catch block to check the error status before
redirecting to login, only redirect if the error indicates an actual
authentication failure (such as 401 or 403 status codes). Additionally, ensure
the `fetchWithAuth` function attaches a status property to thrown errors so the
error handler can reliably determine the failure type and handle non-auth errors
appropriately without redirecting to the login page.

}

export async function adminLoader() {
const data = await dashboardLoader();

if (data instanceof Response) {
return data;
}

if (data?.user?.role !== "ADMIN") {
return redirect("/dashboard");
}

return data;
}

export async function authLoader() {
try {
const response = await fetchWithAuth("/api/me");
Expand All @@ -24,4 +38,3 @@ export async function authLoader() {
return null;
}
}

6 changes: 5 additions & 1 deletion client/src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export function cn(...inputs) {
return twMerge(clsx(...inputs));
}

export function getApiBaseUrl() {
return import.meta.env.VITE_API_URL || "http://localhost:3000";
}

Comment on lines +9 to +12

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Align fallback API port with server default to avoid local breakage.

Line 10 defaults to http://localhost:3000, but server/src/config/env.js defaults the backend to port 3001 (Line 6). If VITE_API_URL is unset, calls routed via Line 148 will target the wrong port and fail.

🔧 Proposed fix
 export function getApiBaseUrl() {
-  return import.meta.env.VITE_API_URL || "http://localhost:3000";
+  return import.meta.env.VITE_API_URL || "http://localhost:3001";
 }

Also applies to: 148-148

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@client/src/lib/utils.js` around lines 9 - 12, The getApiBaseUrl function
returns a hardcoded fallback of http://localhost:3000, but the backend server
defaults to port 3001 according to server/src/config/env.js. When VITE_API_URL
is unset during local development, API calls will fail because they target the
wrong port. Update the fallback URL in the getApiBaseUrl function to use port
3001 instead of 3000 to align with the server's default configuration.

export function formatMoney(amountInMinorUnit, currency) {
return new Intl.NumberFormat("en-NG", {
style: "currency",
Expand Down Expand Up @@ -141,7 +145,7 @@ async function extractErrorMessage(response) {
let refreshPromise = null;

export async function fetchWithAuth(url, options = {}) {
const API_BASE_URL = import.meta.env.VITE_API_URL || "http://localhost:3000";
const API_BASE_URL = getApiBaseUrl();
try {
let response = await fetch(`${API_BASE_URL}${url}`, {
...options,
Expand Down
8 changes: 7 additions & 1 deletion client/src/lib/var.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
History,
Bell,
Settings,
ShieldCheck,

} from "lucide-react";

Expand Down Expand Up @@ -39,6 +40,12 @@ export const navLinks = [
path: "/dashboard/settings",
icon: Settings,
},
{
label: "Admin",
path: "/dashboard/admin",
icon: ShieldCheck,
adminOnly: true,
},
];

export const toneStyles = {
Expand All @@ -61,4 +68,3 @@ export const toneStyles = {
};



10 changes: 9 additions & 1 deletion client/src/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import Overview from "./pages/dashboard/Overview.jsx";
import Settings from "./pages/dashboard/Settings.jsx";
import Notification from "./pages/dashboard/Notification.jsx";
import History from "./pages/dashboard/History.jsx";
import Admin from "./pages/dashboard/Admin.jsx";
import Subscriptions from "./pages/dashboard/Subscriptions.jsx";
import Dashboard from "./layout/Dashboard.jsx";
import Add from "./pages/dashboard/subscription/Add";
import Edit from "./pages/dashboard/subscription/Edit";
import Error from "./pages/Error";
import NotFound from "./pages/NotFound";

import { authLoader, dashboardLoader } from "./lib/loader.js";
import { adminLoader, authLoader, dashboardLoader } from "./lib/loader.js";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

Expand Down Expand Up @@ -71,9 +73,15 @@ const router = createBrowserRouter([
{ path: "subscriptions/:id/edit", element: <Edit /> },
{ path: "notifications", element: <Notification /> },
{ path: "history", element: <History /> },
{ path: "admin", element: <Admin />, loader: adminLoader },
{ path: "settings", element: <Settings /> },
{ path: "*", element: <NotFound /> },
],
},
{
path: "*",
element: <NotFound />,
},
]);

createRoot(document.getElementById("root")).render(
Expand Down
23 changes: 23 additions & 0 deletions client/src/pages/NotFound.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Link } from "react-router";
import { SearchX } from "lucide-react";

import { Button } from "@/components/ui/button";

export default function NotFound() {
return (
<main className="flex min-h-screen items-center justify-center bg-subly-background px-4">
<div className="w-full max-w-md rounded-xl border border-subly-border bg-subly-card p-8 text-center shadow-sm">
<div className="mx-auto flex h-14 w-14 items-center justify-center rounded-xl bg-subly-soft-blue text-subly-brand-blue">
<SearchX size={26} />
</div>
<h1 className="mt-5 text-3xl font-bold text-subly-text-primary">404</h1>
<p className="mt-2 text-sm text-subly-text-secondary">
The page you are looking for does not exist.
</p>
<Button asChild className="mt-6">
<Link to="/dashboard">Go to dashboard</Link>
</Button>
</div>
</main>
);
}
Loading