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:
parent
deae9c0520
commit
59cf55ae95
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
7
apps/portal/src/app/(portal)/layout.tsx
Normal file
7
apps/portal/src/app/(portal)/layout.tsx
Normal 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>;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
@ -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"],
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user