Refactor layout components and enforce import restrictions in portal pages

- Removed direct imports of DashboardLayout from various portal pages to enforce the use of shared layout components.
- Introduced ESLint rules to prevent importing DashboardLayout directly in (portal) pages, encouraging the use of the shared route-group layout.
- Updated navigation methods to utilize Next.js router instead of window.location for improved routing consistency.
- Enhanced loading states and error handling across multiple pages for better user experience.
This commit is contained in:
T. Narantuya 2025-09-11 14:23:33 +09:00
parent deae9c0520
commit 59cf55ae95
18 changed files with 188 additions and 184 deletions

View File

@ -1,7 +1,6 @@
"use client";
import { useState, useEffect, Suspense } from "react";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { useSearchParams } from "next/navigation";
import { authenticatedApi } from "@/lib/api";
import {
@ -151,21 +150,17 @@ export default function BillingPage() {
if (loading) {
return (
<DashboardLayout>
<div className="max-w-4xl mx-auto">
<div className="flex items-center space-x-3 mb-6">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="text-xl font-semibold text-gray-900">
Loading billing information...
</span>
</div>
<div className="max-w-4xl mx-auto">
<div className="flex items-center space-x-3 mb-6">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="text-xl font-semibold text-gray-900">Loading billing information...</span>
</div>
</DashboardLayout>
</div>
);
}
return (
<DashboardLayout>
<>
<div className="max-w-4xl mx-auto">
<div className="flex items-center space-x-3 mb-6">
<CreditCardIcon className="h-8 w-8 text-blue-600" />
@ -431,6 +426,6 @@ export default function BillingPage() {
</div>
</div>
</div>
</DashboardLayout>
</>
);
}

View File

@ -1,7 +1,6 @@
"use client";
import { useState, useEffect } from "react";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { useAuthStore } from "@/lib/auth/store";
import { authenticatedApi } from "@/lib/api";
import { logger } from "@/lib/logger";
@ -278,23 +277,20 @@ export default function ProfilePage() {
if (loading) {
return (
<DashboardLayout>
<div className="py-6">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">Loading profile...</span>
</div>
<div className="py-6">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
<span className="ml-3 text-gray-600">Loading profile...</span>
</div>
</div>
</DashboardLayout>
</div>
);
}
return (
<DashboardLayout>
<div className="py-6">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
<div className="py-6">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center space-x-4">
@ -760,8 +756,7 @@ export default function ProfilePage() {
</div>
</div>
</div>
</div>
</div>
</DashboardLayout>
</div>
);
}

View File

@ -4,7 +4,6 @@ import { logger } from "@/lib/logger";
import { useState } from "react";
import { useParams } from "next/navigation";
import Link from "next/link";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { SubCard } from "@/components/ui/sub-card";
import { StatusPill } from "@/components/ui/status-pill";
import { useAuthStore } from "@/lib/auth/store";
@ -117,49 +116,42 @@ export default function InvoiceDetailPage() {
if (isLoading) {
return (
<DashboardLayout>
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading invoice...</p>
</div>
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading invoice...</p>
</div>
</DashboardLayout>
</div>
);
}
if (error || !invoice) {
return (
<DashboardLayout>
<div className="space-y-6">
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationTriangleIcon className="h-5 w-5 text-red-400" />
<div className="space-y-6">
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationTriangleIcon className="h-5 w-5 text-red-400" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Error loading invoice</h3>
<div className="mt-2 text-sm text-red-700">
{error instanceof Error ? error.message : "Invoice not found"}
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Error loading invoice</h3>
<div className="mt-2 text-sm text-red-700">
{error instanceof Error ? error.message : "Invoice not found"}
</div>
<div className="mt-4">
<Link
href="/billing/invoices"
className="text-red-700 hover:text-red-600 font-medium"
>
Back to invoices
</Link>
</div>
<div className="mt-4">
<Link href="/billing/invoices" className="text-red-700 hover:text-red-600 font-medium">
Back to invoices
</Link>
</div>
</div>
</div>
</div>
</DashboardLayout>
</div>
);
}
return (
<DashboardLayout>
<>
<div className="py-8">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
{/* Back Button */}
@ -377,6 +369,6 @@ export default function InvoiceDetailPage() {
</div>
</div>
</div>
</DashboardLayout>
</>
);
}

View File

@ -1,6 +1,7 @@
"use client";
import React, { useState, useMemo } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { PageLayout } from "@/components/layout/page-layout";
import { SubCard } from "@/components/ui/sub-card";
@ -24,6 +25,7 @@ import type { Invoice } from "@customer-portal/shared";
import { formatCurrency, getCurrencyLocale } from "@/utils/currency";
export default function InvoicesPage() {
const router = useRouter();
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
const [currentPage, setCurrentPage] = useState(1);
@ -293,7 +295,7 @@ export default function InvoicesPage() {
? "No invoices match your filter criteria."
: "No invoices have been generated yet.",
}}
onRowClick={invoice => (window.location.href = `/billing/invoices/${invoice.id}`)}
onRowClick={invoice => router.push(`/billing/invoices/${invoice.id}`)}
/>
</SubCard>

View File

@ -3,8 +3,8 @@ import { logger } from "@/lib/logger";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useAuthStore } from "@/lib/auth/store";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { useDashboardSummary } from "@/features/dashboard/hooks";
import type { Activity } from "@customer-portal/shared";
@ -34,6 +34,7 @@ import { ErrorState } from "@/components/ui/error-state";
import { formatCurrency, getCurrencyLocale } from "@/utils/currency";
export default function DashboardPage() {
const router = useRouter();
const { user, isAuthenticated, isLoading: authLoading } = useAuthStore();
const { data: summary, isLoading: summaryLoading, error } = useDashboardSummary();
@ -64,39 +65,35 @@ export default function DashboardPage() {
if (activity.type === "invoice_created" || activity.type === "invoice_paid") {
// Use the related invoice ID for navigation
if (activity.relatedId) {
window.location.href = `/billing/invoices/${activity.relatedId}`;
router.push(`/billing/invoices/${activity.relatedId}`);
}
}
};
if (authLoading || summaryLoading || !isAuthenticated) {
return (
<DashboardLayout>
<div className="flex items-center justify-center h-64">
<div className="text-center space-y-4">
<LoadingSpinner size="lg" />
<p className="text-muted-foreground">Loading dashboard...</p>
</div>
<div className="flex items-center justify-center h-64">
<div className="text-center space-y-4">
<LoadingSpinner size="lg" />
<p className="text-muted-foreground">Loading dashboard...</p>
</div>
</DashboardLayout>
</div>
);
}
// Handle error state
if (error) {
return (
<DashboardLayout>
<ErrorState
title="Error loading dashboard"
message={error instanceof Error ? error.message : "An unexpected error occurred"}
variant="page"
/>
</DashboardLayout>
<ErrorState
title="Error loading dashboard"
message={error instanceof Error ? error.message : "An unexpected error occurred"}
variant="page"
/>
);
}
return (
<DashboardLayout>
<>
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-blue-50">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8 py-[var(--cp-space-2xl)]">
{/* Modern Header */}
@ -276,7 +273,7 @@ export default function DashboardPage() {
</div>
</div>
</div>
</DashboardLayout>
</>
);
}
@ -287,6 +284,7 @@ function truncateName(name: string, len = 28) {
}
function TasksChip({ summaryLoading, summary }: { summaryLoading: boolean; summary: any }) {
const router = useRouter();
if (summaryLoading) return null;
const tasks: Array<{ label: string; href: string }> = [];
if (summary?.nextInvoice) tasks.push({ label: "Pay upcoming invoice", href: "#attention" });
@ -300,7 +298,7 @@ function TasksChip({ summaryLoading, summary }: { summaryLoading: boolean; summa
const el = document.querySelector(first.href);
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
} else {
window.location.href = first.href;
router.push(first.href);
}
}}
className="inline-flex items-center rounded-full bg-blue-50 text-blue-700 px-2.5 py-1 text-xs font-medium hover:bg-blue-100"

View File

@ -0,0 +1,7 @@
import type { ReactNode } from "react";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
export default function PortalLayout({ children }: { children: ReactNode }) {
return <DashboardLayout>{children}</DashboardLayout>;
}

View File

@ -1,9 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import { useParams, useSearchParams } from "next/navigation";
import { useParams, useSearchParams, useRouter } from "next/navigation";
import Link from "next/link";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import {
ArrowLeftIcon,
ServerIcon,
@ -21,6 +20,7 @@ import { formatCurrency as sharedFormatCurrency, getCurrencyLocale } from "@/uti
import { SimManagementSection } from "@/features/sim-management";
export default function SubscriptionDetailPage() {
const router = useRouter();
const params = useParams();
const searchParams = useSearchParams();
const [currentPage, setCurrentPage] = useState(1);
@ -159,49 +159,42 @@ export default function SubscriptionDetailPage() {
if (isLoading) {
return (
<DashboardLayout>
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading subscription...</p>
</div>
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading subscription...</p>
</div>
</DashboardLayout>
</div>
);
}
if (error || !subscription) {
return (
<DashboardLayout>
<div className="space-y-6">
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationTriangleIcon className="h-5 w-5 text-red-400" />
<div className="space-y-6">
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<div className="flex">
<div className="flex-shrink-0">
<ExclamationTriangleIcon className="h-5 w-5 text-red-400" />
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Error loading subscription</h3>
<div className="mt-2 text-sm text-red-700">
{error instanceof Error ? error.message : "Subscription not found"}
</div>
<div className="ml-3">
<h3 className="text-sm font-medium text-red-800">Error loading subscription</h3>
<div className="mt-2 text-sm text-red-700">
{error instanceof Error ? error.message : "Subscription not found"}
</div>
<div className="mt-4">
<Link
href="/subscriptions"
className="text-red-700 hover:text-red-600 font-medium"
>
Back to subscriptions
</Link>
</div>
<div className="mt-4">
<Link href="/subscriptions" className="text-red-700 hover:text-red-600 font-medium">
Back to subscriptions
</Link>
</div>
</div>
</div>
</div>
</DashboardLayout>
</div>
);
}
return (
<DashboardLayout>
<>
<div className="py-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
{/* Header */}
@ -402,9 +395,7 @@ export default function SubscriptionDetailPage() {
</span>
</div>
<button
onClick={() =>
(window.location.href = `/billing/invoices/${invoice.id}`)
}
onClick={() => router.push(`/billing/invoices/${invoice.id}`)}
className="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all duration-200 shadow-sm hover:shadow-md"
>
<DocumentTextIcon className="h-4 w-4 mr-2" />
@ -499,6 +490,6 @@ export default function SubscriptionDetailPage() {
)}
</div>
</div>
</DashboardLayout>
</>
);
}

View File

@ -1,7 +1,6 @@
"use client";
import Link from "next/link";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { useParams } from "next/navigation";
import { useState } from "react";
import { authenticatedApi } from "@/lib/api";
@ -28,8 +27,7 @@ export default function SimCancelPage() {
};
return (
<DashboardLayout>
<div className="max-w-3xl mx-auto p-6">
<div className="max-w-3xl mx-auto p-6">
<div className="mb-4">
<Link
href={`/subscriptions/${subscriptionId}#sim-management`}
@ -77,6 +75,5 @@ export default function SimCancelPage() {
</div>
</div>
</div>
</DashboardLayout>
);
}

View File

@ -2,7 +2,6 @@
import { useState, useMemo } from "react";
import Link from "next/link";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { useParams } from "next/navigation";
import { authenticatedApi } from "@/lib/api";
@ -55,8 +54,7 @@ export default function SimChangePlanPage() {
};
return (
<DashboardLayout>
<div className="max-w-3xl mx-auto p-6">
<div className="max-w-3xl mx-auto p-6">
<div className="mb-4">
<Link
href={`/subscriptions/${subscriptionId}#sim-management`}
@ -143,6 +141,5 @@ export default function SimChangePlanPage() {
</form>
</div>
</div>
</DashboardLayout>
);
}

View File

@ -2,7 +2,6 @@
import { useState } from "react";
import Link from "next/link";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import { useParams } from "next/navigation";
import { authenticatedApi } from "@/lib/api";
@ -40,8 +39,7 @@ export default function SimTopUpPage() {
};
return (
<DashboardLayout>
<div className="max-w-3xl mx-auto p-6">
<div className="max-w-3xl mx-auto p-6">
<div className="mb-4">
<Link
href={`/subscriptions/${subscriptionId}#sim-management`}
@ -128,6 +126,5 @@ export default function SimTopUpPage() {
</form>
</div>
</div>
</DashboardLayout>
);
}

View File

@ -1,6 +1,7 @@
"use client";
import { useState, useMemo } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { PageLayout } from "@/components/layout/page-layout";
import { DataTable } from "@/components/ui/data-table";
@ -26,6 +27,7 @@ import type { Subscription } from "@customer-portal/shared";
// Removed unused SubscriptionStatusBadge in favor of StatusPill
export default function SubscriptionsPage() {
const router = useRouter();
const [searchTerm, setSearchTerm] = useState("");
const [statusFilter, setStatusFilter] = useState("all");
@ -328,7 +330,7 @@ export default function SubscriptionsPage() {
? "Try adjusting your search or filter criteria."
: "No active subscriptions at this time.",
}}
onRowClick={subscription => (window.location.href = `/subscriptions/${subscription.id}`)}
onRowClick={subscription => router.push(`/subscriptions/${subscription.id}`)}
/>
</SubCard>
</PageLayout>

View File

@ -2,7 +2,6 @@
import { useState, useEffect } from "react";
import Link from "next/link";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import {
ChatBubbleLeftRightIcon,
MagnifyingGlassIcon,
@ -170,19 +169,17 @@ export default function SupportCasesPage() {
if (loading) {
return (
<DashboardLayout>
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading support cases...</p>
</div>
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
<p className="mt-4 text-gray-600">Loading support cases...</p>
</div>
</DashboardLayout>
</div>
);
}
return (
<DashboardLayout>
<>
<div className="py-6">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
{/* Header */}
@ -420,6 +417,6 @@ export default function SupportCasesPage() {
</div>
</div>
</div>
</DashboardLayout>
</>
);
}

View File

@ -4,7 +4,6 @@ import { logger } from "@/lib/logger";
import { useState } from "react";
import { useRouter } from "next/navigation";
import Link from "next/link";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
import {
ArrowLeftIcon,
PaperAirplaneIcon,
@ -51,9 +50,8 @@ export default function NewSupportCasePage() {
const isFormValid = formData.subject.trim() && formData.description.trim();
return (
<DashboardLayout>
<div className="py-6">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
<div className="py-6">
<div className="max-w-4xl mx-auto px-4 sm:px-6 md:px-8">
{/* Header */}
<div className="mb-8">
<div className="flex items-center space-x-4 mb-4">
@ -258,6 +256,5 @@ export default function NewSupportCasePage() {
</div>
</div>
</div>
</DashboardLayout>
);
}

View File

@ -172,7 +172,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
}
return (
<div className="h-screen flex overflow-hidden bg-background">
<div className="h-screen flex overflow-hidden bg-background">
{/* Mobile sidebar overlay */}
{sidebarOpen && (
@ -228,17 +228,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
<Bars3Icon className="h-6 w-6" />
</button>
{/* Brand / Home */}
<div className="hidden md:flex items-center">
<Link
href="/dashboard"
className="inline-flex items-center text-sm font-semibold text-gray-900 hover:text-primary"
aria-label="Home"
>
<HomeIcon className="h-5 w-5 mr-2" />
Portal
</Link>
</div>
{/* Brand removed from header per design */}
{/* Spacer */}
<div className="flex-1" />
@ -275,7 +265,7 @@ export function DashboardLayout({ children }: DashboardLayoutProps) {
<main className="flex-1 relative overflow-y-auto focus:outline-none">{children}</main>
</div>
</div>
</div>
);
}
@ -339,8 +329,8 @@ const DesktopSidebar = memo(function DesktopSidebar({
<Logo size={20} />
</div>
<div>
<span className="text-base font-bold text-[var(--cp-sidebar-text)]">Portal</span>
<p className="text-xs text-[var(--cp-sidebar-text)]/60">Assist Solutions</p>
<span className="text-base font-bold text-[var(--cp-sidebar-text)]">Assist Solutions</span>
<p className="text-xs text-[var(--cp-sidebar-text)]/60">Customer Portal</p>
</div>
</div>
</div>
@ -383,8 +373,8 @@ const MobileSidebar = memo(function MobileSidebar({
<Logo size={20} />
</div>
<div>
<span className="text-base font-bold text-[var(--cp-sidebar-text)]">Portal</span>
<p className="text-xs text-[var(--cp-sidebar-text)]/60">Assist Solutions</p>
<span className="text-base font-bold text-[var(--cp-sidebar-text)]">Assist Solutions</span>
<p className="text-xs text-[var(--cp-sidebar-text)]/60">Customer Portal</p>
</div>
</div>
</div>

View File

@ -1,5 +1,4 @@
import type { ReactNode } from "react";
import { DashboardLayout } from "@/components/layout/dashboard-layout";
interface PageLayoutProps {
icon: ReactNode;
@ -11,37 +10,31 @@ interface PageLayoutProps {
export function PageLayout({ icon, title, description, actions, children }: PageLayoutProps) {
return (
<DashboardLayout>
<div className="py-[var(--cp-space-xl)] sm:py-[var(--cp-space-2xl)]">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-lg)] sm:px-[var(--cp-page-padding)] md:px-8">
{/* Header */}
<div className="mb-[var(--cp-space-2xl)] sm:mb-[var(--cp-space-3xl)]">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-[var(--cp-space-lg)] sm:gap-[var(--cp-space-xl)]">
<div className="flex items-start min-w-0 flex-1">
{icon && (
<div className="h-8 w-8 text-primary mr-[var(--cp-space-lg)] flex-shrink-0 mt-1">
{icon}
</div>
<div className="py-[var(--cp-space-xl)] sm:py-[var(--cp-space-2xl)]">
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-space-lg)] sm:px-[var(--cp-page-padding)] md:px-8">
{/* Header */}
<div className="mb-[var(--cp-space-2xl)] sm:mb-[var(--cp-space-3xl)]">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-[var(--cp-space-lg)] sm:gap-[var(--cp-space-xl)]">
<div className="flex items-start min-w-0 flex-1">
{icon && (
<div className="h-8 w-8 text-primary mr-[var(--cp-space-lg)] flex-shrink-0 mt-1">{icon}</div>
)}
<div className="min-w-0 flex-1">
<h1 className="text-2xl sm:text-3xl font-bold text-foreground leading-tight">{title}</h1>
{description && (
<p className="text-sm sm:text-base text-muted-foreground mt-1 leading-relaxed">
{description}
</p>
)}
<div className="min-w-0 flex-1">
<h1 className="text-2xl sm:text-3xl font-bold text-foreground leading-tight">
{title}
</h1>
{description && (
<p className="text-sm sm:text-base text-muted-foreground mt-1 leading-relaxed">
{description}
</p>
)}
</div>
</div>
{actions && <div className="flex-shrink-0 w-full sm:w-auto">{actions}</div>}
</div>
{actions && <div className="flex-shrink-0 w-full sm:w-auto">{actions}</div>}
</div>
{/* Content */}
<div className="space-y-[var(--cp-space-2xl)]">{children}</div>
</div>
{/* Content */}
<div className="space-y-[var(--cp-space-2xl)]">{children}</div>
</div>
</DashboardLayout>
</div>
);
}

View File

@ -1,5 +1,6 @@
"use client";
import { formatCurrency } from "@/utils/currency";
import { useRouter } from "next/navigation";
export function InvoiceItemRow({
id,
@ -16,6 +17,7 @@ export function InvoiceItemRow({
quantity?: number;
serviceId?: number;
}) {
const router = useRouter();
return (
<div
key={id}
@ -24,7 +26,7 @@ export function InvoiceItemRow({
? "border-blue-200 bg-blue-50 hover:bg-blue-100 cursor-pointer hover:shadow-sm"
: "border-gray-200 bg-gray-50"
}`}
onClick={serviceId ? () => (window.location.href = `/subscriptions/${serviceId}`) : undefined}
onClick={serviceId ? () => router.push(`/subscriptions/${serviceId}`) : undefined}
>
<div className="flex-1 min-w-0">
<div

View File

@ -1,6 +1,7 @@
"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
export function StatCard({
title,
@ -17,6 +18,7 @@ export function StatCard({
href: string;
zeroHint?: { text: string; href: string };
}) {
const router = useRouter();
return (
<Link href={href} className="group">
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden hover:shadow-xl transition-all duration-300 group-hover:scale-[1.02] min-h-[116px] flex">
@ -34,7 +36,7 @@ export function StatCard({
// Prevent card navigation if clicking the hint
e.preventDefault();
e.stopPropagation();
window.location.href = zeroHint.href;
router.push(zeroHint.href);
}}
className="mt-1 inline-flex items-center text-xs font-medium text-blue-600 hover:text-blue-700 cursor-pointer"
>

View File

@ -99,6 +99,56 @@ export default [
},
},
// Prevent importing the DashboardLayout directly in (portal) pages.
// Pages should rely on the shared route-group layout at (portal)/layout.tsx.
{
files: ["apps/portal/src/app/(portal)/**/*.{ts,tsx}"],
rules: {
"no-restricted-imports": [
"error",
{
patterns: [
{
group: ["@/components/layout/dashboard-layout"],
message:
"Use the shared (portal)/layout.tsx instead of importing DashboardLayout in pages.",
},
],
},
],
// Prefer Next.js <Link> and router, forbid window/location hard reload in portal pages
"no-restricted-syntax": [
"error",
{
selector:
"MemberExpression[object.name='window'][property.name='location']",
message:
"Use next/link or useRouter for navigation, not window.location.",
},
{
selector:
"MemberExpression[object.name='location'][property.name=/^(href|assign|replace)$/]",
message:
"Use next/link or useRouter for navigation, not location.*.",
},
],
},
},
// Allow the shared layout file itself to import the layout component
{
files: ["apps/portal/src/app/(portal)/layout.tsx"],
rules: {
"no-restricted-imports": "off",
},
},
// Allow controlled window.location usage for invoice SSO download
{
files: ["apps/portal/src/app/(portal)/billing/invoices/[id]/page.tsx"],
rules: {
"no-restricted-syntax": "off",
},
},
// Node globals for Next config file
{
files: ["apps/portal/next.config.mjs"],