262 lines
7.3 KiB
TypeScript
262 lines
7.3 KiB
TypeScript
import { NavigationGroup, NavigationItem } from "./welcome-types.ts";
|
|
|
|
export type NavigationContext = "calendar" | "landlord" | "tenant" | "default";
|
|
|
|
function normalizePath(path: string): string {
|
|
if (!path) return "/";
|
|
|
|
const [pathname] = path.split(/[?#]/);
|
|
if (!pathname || pathname === "/") return "/";
|
|
|
|
return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
|
|
}
|
|
|
|
/** Placeholder replaced at runtime with server or client calendar path for hydration-safe nav. */
|
|
export const CALENDAR_PATH_PLACEHOLDER = "__SERVER_CALENDAR_PATH__";
|
|
|
|
function getCurrentFinancialCalendarPath(date = new Date()): string {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
return `/calendar/financial-calendar/${year}/${month}`;
|
|
}
|
|
|
|
export function resolveNavigationContext(
|
|
currentRoute: string,
|
|
): NavigationContext {
|
|
const route = normalizePath(currentRoute);
|
|
|
|
if (route.startsWith("/calendar/")) return "calendar";
|
|
if (route.startsWith("/landlord/")) return "landlord";
|
|
if (route.startsWith("/tenant/")) return "tenant";
|
|
|
|
return "default";
|
|
}
|
|
|
|
export function isNavigationItemActive(
|
|
item: NavigationItem,
|
|
currentRoute: string,
|
|
): boolean {
|
|
if (item.isForm) return false;
|
|
|
|
const route = normalizePath(currentRoute);
|
|
const itemPath = normalizePath(item.path);
|
|
|
|
if (route === itemPath) {
|
|
return true;
|
|
}
|
|
|
|
if (item.matchPrefixes && item.matchPrefixes.length > 0) {
|
|
return item.matchPrefixes.some((prefix) => {
|
|
const normalizedPrefix = normalizePath(prefix);
|
|
return route === normalizedPrefix ||
|
|
route.startsWith(`${normalizedPrefix}/`);
|
|
});
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Default navigation groups configuration.
|
|
* Groups are filtered by current route context (calendar / landlord / tenant / default).
|
|
*/
|
|
export const defaultNavigationGroups: NavigationGroup[] = [
|
|
{
|
|
id: "auth",
|
|
label: "Session",
|
|
items: [
|
|
{
|
|
path: "/auth/login",
|
|
label: "Sign In",
|
|
requiresAuth: false,
|
|
isForm: true,
|
|
formAction: "/auth/login",
|
|
formMethod: "get",
|
|
},
|
|
{
|
|
path: "/auth/logout",
|
|
label: "Logout",
|
|
requiresAuth: true,
|
|
isForm: true,
|
|
formAction: "/auth/logout",
|
|
formMethod: "post",
|
|
onClick: () => {
|
|
localStorage.setItem("lastClickedButton", "logout");
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "apps",
|
|
label: "App Switch",
|
|
items: [
|
|
{
|
|
path: CALENDAR_PATH_PLACEHOLDER,
|
|
label: "Calendar",
|
|
requiresAuth: true,
|
|
matchPrefixes: ["/calendar/financial-calendar"],
|
|
},
|
|
{
|
|
path: "/landlord/dashboard",
|
|
label: "Landlord",
|
|
requiresAuth: true,
|
|
},
|
|
{
|
|
path: "/tenant/dashboard",
|
|
label: "Tenant",
|
|
requiresAuth: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "calendar",
|
|
label: "Calendar",
|
|
items: [
|
|
{
|
|
path: CALENDAR_PATH_PLACEHOLDER,
|
|
label: "Financial Calendar",
|
|
requiresAuth: true,
|
|
matchPrefixes: ["/calendar/financial-calendar"],
|
|
},
|
|
{
|
|
path: "/calendar/every-income",
|
|
label: "Every Income",
|
|
requiresAuth: true,
|
|
},
|
|
{
|
|
path: "/calendar/budget-obligations",
|
|
label: "Budget Obligations",
|
|
requiresAuth: true,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
id: "landlord",
|
|
label: "Landlord",
|
|
items: [
|
|
{ path: "/landlord/dashboard", label: "Dashboard", requiresAuth: true },
|
|
{ path: "/landlord/properties", label: "Properties", requiresAuth: true },
|
|
{ path: "/landlord/leases", label: "Leases", requiresAuth: true },
|
|
{ path: "/landlord/payments", label: "Payments", requiresAuth: true },
|
|
{
|
|
path: "/landlord/maintenance",
|
|
label: "Maintenance",
|
|
requiresAuth: true,
|
|
},
|
|
{
|
|
path: "/landlord/applications",
|
|
label: "Applications",
|
|
requiresAuth: true,
|
|
},
|
|
{ path: "/landlord/messages", label: "Messages", requiresAuth: true },
|
|
{ path: "/landlord/reminders", label: "Reminders", requiresAuth: true },
|
|
{ path: "/landlord/documents", label: "Documents", requiresAuth: true },
|
|
],
|
|
},
|
|
{
|
|
id: "tenant",
|
|
label: "Tenant",
|
|
items: [
|
|
{ path: "/tenant/dashboard", label: "Dashboard", requiresAuth: true },
|
|
{ path: "/tenant/property", label: "Property", requiresAuth: true },
|
|
{ path: "/tenant/payments", label: "Payments", requiresAuth: true },
|
|
{
|
|
path: "/tenant/payment-methods",
|
|
label: "Payment Methods",
|
|
requiresAuth: true,
|
|
},
|
|
{ path: "/tenant/maintenance", label: "Maintenance", requiresAuth: true },
|
|
{ path: "/tenant/messages", label: "Messages", requiresAuth: true },
|
|
{
|
|
path: "/tenant/applications",
|
|
label: "Applications",
|
|
requiresAuth: true,
|
|
},
|
|
{ path: "/tenant/reminders", label: "Reminders", requiresAuth: true },
|
|
{ path: "/tenant/documents", label: "Documents", requiresAuth: true },
|
|
{ path: "/tenant/settings", label: "Settings", requiresAuth: true },
|
|
],
|
|
},
|
|
{
|
|
id: "general",
|
|
label: "General",
|
|
items: [
|
|
{
|
|
path: "/",
|
|
label: "Home",
|
|
requiresAuth: false,
|
|
},
|
|
{
|
|
path: "/user-profile",
|
|
label: "Profile",
|
|
requiresAuth: true,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const contextGroupOrder: Record<NavigationContext, string[]> = {
|
|
default: ["auth", "apps", "general"],
|
|
calendar: ["auth", "calendar", "apps", "general"],
|
|
landlord: ["auth", "landlord", "apps", "general"],
|
|
tenant: ["auth", "tenant", "apps", "general"],
|
|
};
|
|
|
|
/**
|
|
* Get navigation groups filtered by authentication status and current route context.
|
|
* serverCalendarPath: use when provided so server and client render the same calendar links (avoids hydration mismatch).
|
|
*/
|
|
export function getNavigationGroups(
|
|
isLoggedIn: boolean,
|
|
currentRoute: string,
|
|
customGroups?: NavigationGroup[],
|
|
serverCalendarPath?: string,
|
|
): NavigationGroup[] {
|
|
const calendarPath = serverCalendarPath ?? getCurrentFinancialCalendarPath();
|
|
const groups = customGroups || defaultNavigationGroups;
|
|
const route = normalizePath(currentRoute);
|
|
const context = resolveNavigationContext(route);
|
|
const orderedGroupIds = contextGroupOrder[context];
|
|
|
|
const substituteCalendarPath = (items: NavigationItem[]): NavigationItem[] =>
|
|
items.map((item) =>
|
|
item.path === CALENDAR_PATH_PLACEHOLDER
|
|
? { ...item, path: calendarPath }
|
|
: item
|
|
);
|
|
|
|
return groups
|
|
.filter((group) => {
|
|
if (customGroups) return true;
|
|
return orderedGroupIds.includes(group.id);
|
|
})
|
|
.sort((a, b) => {
|
|
if (customGroups) return 0;
|
|
return orderedGroupIds.indexOf(a.id) - orderedGroupIds.indexOf(b.id);
|
|
})
|
|
.map((group) => ({
|
|
...group,
|
|
items: substituteCalendarPath(group.items).filter((item) => {
|
|
if (item.requiresAuth && !isLoggedIn) {
|
|
return false;
|
|
}
|
|
|
|
if (group.id === "auth") {
|
|
if (item.path === "/auth/login" && isLoggedIn) {
|
|
return false;
|
|
}
|
|
if (item.path === "/auth/logout" && !isLoggedIn) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (isNavigationItemActive(item, route)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}),
|
|
}))
|
|
.filter((group) => group.items.length > 0);
|
|
}
|