refactor: layer portal routes through feature modules
This commit is contained in:
parent
143aad11d8
commit
e9acbd899c
@ -6,26 +6,31 @@ This document outlines the new feature-driven architecture implemented for the c
|
|||||||
|
|
||||||
```
|
```
|
||||||
apps/portal/src/
|
apps/portal/src/
|
||||||
├── app/ # Next.js App Router pages
|
├── app/ # Next.js App Router entry points (route groups only)
|
||||||
├── components/ # Shared UI components (Design System)
|
│ ├── (public)/ # Marketing + auth routes, pages import feature views
|
||||||
│ ├── ui/ # Base UI components (atoms)
|
│ ├── (authenticated)/ # Signed-in portal routes, thin wrappers around features
|
||||||
│ ├── layout/ # Layout components (organisms)
|
│ ├── api/ # App Router API routes
|
||||||
│ └── common/ # Shared business components (molecules)
|
│ ├── favicon.ico / globals.css # Global assets
|
||||||
├── features/ # Feature-specific modules
|
│ └── layout.tsx # Root layout/providers
|
||||||
│ ├── auth/ # Authentication feature
|
├── components/ # Shared UI components (design system atoms/molecules)
|
||||||
│ ├── dashboard/ # Dashboard feature
|
│ ├── ui/
|
||||||
│ ├── billing/ # Billing feature
|
│ ├── layout/
|
||||||
│ ├── subscriptions/ # Subscriptions feature
|
│ └── common/
|
||||||
│ ├── catalog/ # Product catalog feature
|
├── core/ # App-wide configuration (env, logger, providers)
|
||||||
│ └── support/ # Support feature
|
├── features/ # Feature-specific modules composed by routes
|
||||||
├── lib/ # Core utilities and services (replaces core/shared)
|
│ ├── account/
|
||||||
│ ├── api/ # API client and base services
|
│ ├── auth/
|
||||||
│ ├── query.ts # Query client and keys
|
│ ├── billing/
|
||||||
│ ├── env.ts # Runtime env parsing
|
│ ├── catalog/
|
||||||
│ ├── types/ # Shared TypeScript types
|
│ ├── dashboard/
|
||||||
│ └── utils/ # Utility functions (cn, currency, error-display, ...)
|
│ ├── marketing/
|
||||||
├── providers/ # React context providers (e.g., QueryProvider)
|
│ ├── orders/
|
||||||
└── styles/ # Global styles and design tokens
|
│ ├── service-management/
|
||||||
|
│ ├── subscriptions/
|
||||||
|
│ └── support/
|
||||||
|
├── shared/ # Cross-feature helpers (e.g., constants, locale data)
|
||||||
|
├── styles/ # Global styles and design tokens
|
||||||
|
└── types/ # Portal-specific TypeScript types
|
||||||
```
|
```
|
||||||
|
|
||||||
## Design Principles
|
## Design Principles
|
||||||
@ -48,7 +53,7 @@ Each feature module contains:
|
|||||||
- `utils/`: Utility functions
|
- `utils/`: Utility functions
|
||||||
|
|
||||||
### 4. Centralized Shared Resources
|
### 4. Centralized Shared Resources
|
||||||
Common utilities, types, and components are centralized in the `lib/` and `components/` directories.
|
Common utilities, types, and components are centralized in the `core/`, `shared/`, and `components/` directories.
|
||||||
|
|
||||||
## Feature Module Structure
|
## Feature Module Structure
|
||||||
|
|
||||||
@ -118,20 +123,21 @@ import { DataTable } from '@/components/common';
|
|||||||
import type { User, ApiResponse } from '@/types';
|
import type { User, ApiResponse } from '@/types';
|
||||||
|
|
||||||
// Utility imports
|
// Utility imports
|
||||||
import { designSystem } from '@/lib/design-system';
|
import { QueryProvider } from '@/core/providers';
|
||||||
// Prefer feature services/hooks over direct apiClient usage in pages
|
// Prefer feature services/hooks over direct api usage in pages
|
||||||
import { apiClient } from '@/lib/api/client';
|
import { logger } from '@/core/config';
|
||||||
```
|
```
|
||||||
|
|
||||||
### Path Mappings
|
### Path Mappings
|
||||||
|
|
||||||
- `@/*` - Root src directory
|
- `@/*` - Root src directory
|
||||||
- `@/components/*` - Component library
|
- `@/components/*` - Component library
|
||||||
|
- `@/core/*` - App-wide configuration and providers
|
||||||
- `@/features/*` - Feature modules
|
- `@/features/*` - Feature modules
|
||||||
- `@/lib/*` - Core utilities
|
- `@/shared/*` - Shared helpers/constants
|
||||||
- `@/types` - Type definitions
|
|
||||||
- `@/styles/*` - Style files
|
- `@/styles/*` - Style files
|
||||||
- `@shared/*` - Shared package
|
- `@/types/*` - Portal-specific types
|
||||||
|
- `@shared/*` - Shared package exports
|
||||||
|
|
||||||
## Migration Strategy
|
## Migration Strategy
|
||||||
|
|
||||||
@ -179,6 +185,14 @@ The migration to this new architecture will be done incrementally:
|
|||||||
|
|
||||||
This ensures pages remain declarative and the feature layer encapsulates logic.
|
This ensures pages remain declarative and the feature layer encapsulates logic.
|
||||||
|
|
||||||
|
### Route Layering
|
||||||
|
|
||||||
|
- `(public)`: marketing landing and authentication flows. These routes render feature views such as `marketing/PublicLandingView` and `auth` screens while remaining server components by default.
|
||||||
|
- `(authenticated)`: signed-in portal experience. Pages import dashboard, billing, subscriptions, etc. from the feature layer and rely on the shared route-group layout to provide navigation.
|
||||||
|
- `api/`: App Router API endpoints remain colocated under `src/app/api` and can reuse feature services for data access.
|
||||||
|
|
||||||
|
Only `layout.tsx`, `page.tsx`, and `loading.tsx` files live inside the route groups. All reusable UI, hooks, and services live under `src/features/**` to keep routing concerns thin.
|
||||||
|
|
||||||
### Current Feature Hooks/Services
|
### Current Feature Hooks/Services
|
||||||
|
|
||||||
- Catalog
|
- Catalog
|
||||||
@ -191,3 +205,7 @@ This ensures pages remain declarative and the feature layer encapsulates logic.
|
|||||||
- Service: `ordersService` (list/detail/create)
|
- Service: `ordersService` (list/detail/create)
|
||||||
- Account
|
- Account
|
||||||
- Service: `accountService` (`/me/address`)
|
- Service: `accountService` (`/me/address`)
|
||||||
|
- Support
|
||||||
|
- Views: `SupportCasesView`, `NewSupportCaseView` (mock data, ready for API wiring)
|
||||||
|
- Marketing
|
||||||
|
- Views: `PublicLandingView`, `PublicLandingLoadingView`
|
||||||
|
|||||||
5
apps/portal/src/app/(authenticated)/dashboard/page.tsx
Normal file
5
apps/portal/src/app/(authenticated)/dashboard/page.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { DashboardView } from "@/features/dashboard";
|
||||||
|
|
||||||
|
export default function DashboardPage() {
|
||||||
|
return <DashboardView />;
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
import { SupportCasesView } from "@/features/support";
|
||||||
|
|
||||||
|
export default function SupportCasesPage() {
|
||||||
|
return <SupportCasesView />;
|
||||||
|
}
|
||||||
5
apps/portal/src/app/(authenticated)/support/new/page.tsx
Normal file
5
apps/portal/src/app/(authenticated)/support/new/page.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { NewSupportCaseView } from "@/features/support";
|
||||||
|
|
||||||
|
export default function NewSupportCasePage() {
|
||||||
|
return <NewSupportCaseView />;
|
||||||
|
}
|
||||||
@ -1,260 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import { logger } from "@/core/config";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import Link from "next/link";
|
|
||||||
import {
|
|
||||||
ArrowLeftIcon,
|
|
||||||
PaperAirplaneIcon,
|
|
||||||
ExclamationCircleIcon,
|
|
||||||
InformationCircleIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
export default function NewSupportCasePage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
subject: "",
|
|
||||||
category: "Technical",
|
|
||||||
priority: "Medium",
|
|
||||||
description: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsSubmitting(true);
|
|
||||||
|
|
||||||
// Mock submission - would normally send to API
|
|
||||||
void (async () => {
|
|
||||||
try {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
||||||
|
|
||||||
// Redirect to cases list with success message
|
|
||||||
router.push("/support/cases?created=true");
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(error, "Error creating case");
|
|
||||||
} finally {
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputChange = (field: string, value: string) => {
|
|
||||||
setFormData(prev => ({
|
|
||||||
...prev,
|
|
||||||
[field]: value,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFormValid = formData.subject.trim() && formData.description.trim();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<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">
|
|
||||||
<button
|
|
||||||
onClick={() => router.back()}
|
|
||||||
className="inline-flex items-center text-sm text-gray-500 hover:text-gray-700"
|
|
||||||
>
|
|
||||||
<ArrowLeftIcon className="h-4 w-4 mr-1" />
|
|
||||||
Back to Support
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Create Support Case</h1>
|
|
||||||
<p className="mt-1 text-sm text-gray-600">Get help from our support team</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Help Tips */}
|
|
||||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<InformationCircleIcon className="h-5 w-5 text-blue-400" />
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
<h3 className="text-sm font-medium text-blue-800">Before creating a case</h3>
|
|
||||||
<div className="mt-2 text-sm text-blue-700">
|
|
||||||
<ul className="list-disc list-inside space-y-1">
|
|
||||||
<li>Check our knowledge base for common solutions</li>
|
|
||||||
<li>Include relevant error messages or screenshots</li>
|
|
||||||
<li>Provide detailed steps to reproduce the issue</li>
|
|
||||||
<li>Mention your service or subscription if applicable</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Form */}
|
|
||||||
<div className="bg-white shadow rounded-lg">
|
|
||||||
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
|
||||||
{/* Subject */}
|
|
||||||
<div>
|
|
||||||
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Subject *
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="subject"
|
|
||||||
value={formData.subject}
|
|
||||||
onChange={e => handleInputChange("subject", e.target.value)}
|
|
||||||
placeholder="Brief description of your issue"
|
|
||||||
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Category and Priority */}
|
|
||||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="category"
|
|
||||||
className="block text-sm font-medium text-gray-700 mb-2"
|
|
||||||
>
|
|
||||||
Category
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="category"
|
|
||||||
value={formData.category}
|
|
||||||
onChange={e => handleInputChange("category", e.target.value)}
|
|
||||||
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
>
|
|
||||||
<option value="Technical">Technical Support</option>
|
|
||||||
<option value="Billing">Billing Question</option>
|
|
||||||
<option value="General">General Inquiry</option>
|
|
||||||
<option value="Feature Request">Feature Request</option>
|
|
||||||
<option value="Bug Report">Bug Report</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="priority"
|
|
||||||
className="block text-sm font-medium text-gray-700 mb-2"
|
|
||||||
>
|
|
||||||
Priority
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="priority"
|
|
||||||
value={formData.priority}
|
|
||||||
onChange={e => handleInputChange("priority", e.target.value)}
|
|
||||||
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
>
|
|
||||||
<option value="Low">Low - General question</option>
|
|
||||||
<option value="Medium">Medium - Issue affecting work</option>
|
|
||||||
<option value="High">High - Service disruption</option>
|
|
||||||
<option value="Critical">Critical - Complete outage</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="description"
|
|
||||||
className="block text-sm font-medium text-gray-700 mb-2"
|
|
||||||
>
|
|
||||||
Description *
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="description"
|
|
||||||
rows={6}
|
|
||||||
value={formData.description}
|
|
||||||
onChange={e => handleInputChange("description", e.target.value)}
|
|
||||||
placeholder="Please provide a detailed description of your issue, including any error messages and steps to reproduce the problem..."
|
|
||||||
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<p className="mt-2 text-xs text-gray-500">
|
|
||||||
The more details you provide, the faster we can help you.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Priority Warning */}
|
|
||||||
{formData.priority === "Critical" && (
|
|
||||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
|
||||||
<div className="flex">
|
|
||||||
<div className="flex-shrink-0">
|
|
||||||
<ExclamationCircleIcon className="h-5 w-5 text-red-400" />
|
|
||||||
</div>
|
|
||||||
<div className="ml-3">
|
|
||||||
<h3 className="text-sm font-medium text-red-800">
|
|
||||||
Critical Priority Selected
|
|
||||||
</h3>
|
|
||||||
<div className="mt-2 text-sm text-red-700">
|
|
||||||
<p>
|
|
||||||
Critical priority should only be used for complete service outages. For
|
|
||||||
urgent issues that aren't complete outages, please use High priority.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<div className="flex items-center justify-between pt-6 border-t border-gray-200">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => router.back()}
|
|
||||||
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!isFormValid || isSubmitting}
|
|
||||||
className="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
{isSubmitting ? (
|
|
||||||
<>
|
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
||||||
Creating Case...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<PaperAirplaneIcon className="h-4 w-4 mr-2" />
|
|
||||||
Create Case
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Additional Help */}
|
|
||||||
<div className="mt-8 bg-gray-50 rounded-lg p-6">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900 mb-4">Need immediate help?</h3>
|
|
||||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-gray-900">Phone Support</h4>
|
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
|
||||||
9:30-18:00 JST
|
|
||||||
<br />
|
|
||||||
<span className="font-medium text-blue-600">0120-660-470</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h4 className="text-sm font-medium text-gray-900">Knowledge Base</h4>
|
|
||||||
<p className="text-sm text-gray-600 mt-1">
|
|
||||||
Search our help articles for quick solutions
|
|
||||||
<br />
|
|
||||||
<Link
|
|
||||||
href="/support/kb"
|
|
||||||
className="font-medium text-blue-600 hover:text-blue-500"
|
|
||||||
>
|
|
||||||
Browse Knowledge Base
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
5
apps/portal/src/app/(public)/loading.tsx
Normal file
5
apps/portal/src/app/(public)/loading.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { PublicLandingLoadingView } from "@/features/marketing";
|
||||||
|
|
||||||
|
export default function PublicHomeLoading() {
|
||||||
|
return <PublicLandingLoadingView />;
|
||||||
|
}
|
||||||
5
apps/portal/src/app/(public)/page.tsx
Normal file
5
apps/portal/src/app/(public)/page.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { PublicLandingView } from "@/features/marketing";
|
||||||
|
|
||||||
|
export default function PublicHomePage() {
|
||||||
|
return <PublicLandingView />;
|
||||||
|
}
|
||||||
@ -1,288 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
import { Logo } from "@/components/ui/logo";
|
|
||||||
import {
|
|
||||||
ArrowPathIcon,
|
|
||||||
UserIcon,
|
|
||||||
SparklesIcon,
|
|
||||||
CreditCardIcon,
|
|
||||||
Cog6ToothIcon,
|
|
||||||
PhoneIcon,
|
|
||||||
ChartBarIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-blue-100 to-blue-900">
|
|
||||||
{/* Header */}
|
|
||||||
<header className="bg-white/90 backdrop-blur-sm shadow-sm border-b border-blue-100">
|
|
||||||
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
|
|
||||||
<div className="flex justify-between items-center h-16">
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<Logo size={40} />
|
|
||||||
<div>
|
|
||||||
<h1 className="text-xl font-semibold text-gray-900">Assist Solutions</h1>
|
|
||||||
<p className="text-xs text-gray-500">Customer Portal</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-3">
|
|
||||||
<Link
|
|
||||||
href="/auth/login"
|
|
||||||
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors duration-200 font-medium text-sm"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/support"
|
|
||||||
className="text-gray-600 hover:text-gray-900 px-4 py-2 text-sm transition-colors duration-200"
|
|
||||||
>
|
|
||||||
Support
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Hero Section */}
|
|
||||||
<section className="relative py-20 overflow-hidden">
|
|
||||||
{/* Abstract background elements */}
|
|
||||||
<div className="absolute inset-0">
|
|
||||||
<div className="absolute top-20 left-10 w-72 h-72 bg-blue-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"></div>
|
|
||||||
<div
|
|
||||||
className="absolute top-40 right-10 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"
|
|
||||||
style={{ animationDelay: "2s" }}
|
|
||||||
></div>
|
|
||||||
<div
|
|
||||||
className="absolute -bottom-8 left-20 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"
|
|
||||||
style={{ animationDelay: "4s" }}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<h1 className="text-5xl md:text-6xl font-bold text-gray-900 mb-[var(--cp-space-2xl)]">
|
|
||||||
New Assist Solutions Customer Portal
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<a
|
|
||||||
href="#portal-access"
|
|
||||||
className="bg-blue-600 text-white px-10 py-4 rounded-xl hover:bg-blue-700 transition-all duration-300 font-semibold text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1"
|
|
||||||
>
|
|
||||||
Get Started
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Customer Portal Access Section */}
|
|
||||||
<section id="portal-access" className="py-20">
|
|
||||||
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center mb-16">
|
|
||||||
<h2 className="text-3xl font-bold text-gray-900 mb-4">Access Your Portal</h2>
|
|
||||||
<p className="text-lg text-gray-600">Choose the option that applies to you</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[var(--cp-space-3xl)] items-stretch">
|
|
||||||
{/* Existing Customers - Migration */}
|
|
||||||
<div className="bg-white rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-all duration-[var(--cp-transition-slow)] transform hover:-translate-y-2 border-[var(--cp-card-border)]">
|
|
||||||
<div className="text-center flex-1 flex flex-col">
|
|
||||||
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
|
|
||||||
<ArrowPathIcon className="w-8 h-8 text-white" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">Existing Customers</h3>
|
|
||||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
|
||||||
Migrate to our new portal and enjoy enhanced security with modern interface.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-auto">
|
|
||||||
<Link
|
|
||||||
href="/auth/link-whmcs"
|
|
||||||
className="block bg-blue-600 text-white px-8 py-4 rounded-xl hover:bg-blue-700 transition-all duration-300 font-semibold text-lg mb-3 shadow-md hover:shadow-lg"
|
|
||||||
>
|
|
||||||
Migrate Your Account
|
|
||||||
</Link>
|
|
||||||
<p className="text-sm text-gray-500">Takes just a few minutes</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Portal Users */}
|
|
||||||
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 border border-gray-100">
|
|
||||||
<div className="text-center flex-1 flex flex-col">
|
|
||||||
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
|
|
||||||
<UserIcon className="w-8 h-8 text-white" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">Portal Users</h3>
|
|
||||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
|
||||||
Sign in to access your dashboard and manage all your services efficiently.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-auto">
|
|
||||||
<Link
|
|
||||||
href="/auth/login"
|
|
||||||
className="block border-2 border-blue-600 text-blue-600 px-8 py-4 rounded-xl hover:bg-blue-600 hover:text-white transition-all duration-300 font-semibold text-lg mb-3"
|
|
||||||
>
|
|
||||||
Login to Portal
|
|
||||||
</Link>
|
|
||||||
<p className="text-sm text-gray-500">Secure access to your account</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* New Customers */}
|
|
||||||
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 border border-gray-100">
|
|
||||||
<div className="text-center flex-1 flex flex-col">
|
|
||||||
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
|
|
||||||
<SparklesIcon className="w-8 h-8 text-white" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-2xl font-bold text-gray-900 mb-4">New Customers</h3>
|
|
||||||
<p className="text-gray-600 mb-6 leading-relaxed">
|
|
||||||
Create your account and access our full range of IT solutions and services.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="mt-auto">
|
|
||||||
<Link
|
|
||||||
href="/auth/signup"
|
|
||||||
className="block border-2 border-blue-600 text-blue-600 px-8 py-4 rounded-xl hover:bg-blue-600 hover:text-white transition-all duration-300 font-semibold text-lg mb-3"
|
|
||||||
>
|
|
||||||
Create Account
|
|
||||||
</Link>
|
|
||||||
<p className="text-sm text-gray-500">Start your journey with us</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Portal Features Section */}
|
|
||||||
<section className="py-16">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center mb-12">
|
|
||||||
<h2 className="text-3xl font-bold text-gray-900 mb-4">Portal Features</h2>
|
|
||||||
<p className="text-lg text-gray-600">
|
|
||||||
Everything you need to manage your Assist Solutions services
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
||||||
<div className="bg-white rounded-lg p-6 text-center shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
|
|
||||||
<div className="flex justify-center mb-3">
|
|
||||||
<CreditCardIcon className="w-8 h-8 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-semibold mb-2 text-gray-900">Billing & Payments</h3>
|
|
||||||
<p className="text-gray-600 text-sm">
|
|
||||||
View invoices, payment history, and manage billing
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg p-6 text-center shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
|
|
||||||
<div className="flex justify-center mb-3">
|
|
||||||
<Cog6ToothIcon className="w-8 h-8 text-purple-600" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-semibold mb-2 text-gray-900">Service Management</h3>
|
|
||||||
<p className="text-gray-600 text-sm">Control and configure your active services</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg p-6 text-center shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
|
|
||||||
<div className="flex justify-center mb-3">
|
|
||||||
<PhoneIcon className="w-8 h-8 text-pink-600" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-semibold mb-2 text-gray-900">Support Tickets</h3>
|
|
||||||
<p className="text-gray-600 text-sm">Create and track support requests</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white rounded-lg p-6 text-center shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-1">
|
|
||||||
<div className="flex justify-center mb-3">
|
|
||||||
<ChartBarIcon className="w-8 h-8 text-blue-600" />
|
|
||||||
</div>
|
|
||||||
<h3 className="text-lg font-semibold mb-2 text-gray-900">Usage Reports</h3>
|
|
||||||
<p className="text-gray-600 text-sm">Monitor service usage and performance</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Support Section */}
|
|
||||||
<section className="py-16">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center mb-12">
|
|
||||||
<h2 className="text-3xl font-bold text-gray-900 mb-4">Need Help?</h2>
|
|
||||||
<p className="text-lg text-gray-600">Our support team is here to assist you</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
||||||
{/* Contact Details Box */}
|
|
||||||
<div className="bg-white rounded-lg p-8 shadow-lg hover:shadow-xl transition-all duration-300">
|
|
||||||
<h3 className="text-xl font-semibold mb-6 text-gray-900">Contact Details</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
|
||||||
<div className="text-center">
|
|
||||||
<h4 className="font-semibold text-gray-900 mb-2">Phone Support</h4>
|
|
||||||
<p className="text-gray-600 text-sm mb-1">9:30-18:00 JST</p>
|
|
||||||
<p className="text-blue-600 font-medium">0120-660-470</p>
|
|
||||||
<p className="text-gray-500 text-sm">Toll Free within Japan</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-center">
|
|
||||||
<h4 className="font-semibold text-gray-900 mb-2">Email Support</h4>
|
|
||||||
<p className="text-gray-600 text-sm mb-1">Response within 24h</p>
|
|
||||||
<Link
|
|
||||||
href="/contact"
|
|
||||||
className="text-purple-600 font-medium hover:text-purple-700"
|
|
||||||
>
|
|
||||||
Send Message
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Live Chat & Business Hours Box */}
|
|
||||||
<div className="bg-white rounded-lg p-8 shadow-lg hover:shadow-xl transition-all duration-300">
|
|
||||||
<h3 className="text-xl font-semibold mb-6 text-gray-900">
|
|
||||||
Live Chat & Business Hours
|
|
||||||
</h3>
|
|
||||||
<div className="grid grid-cols-2 gap-6">
|
|
||||||
<div className="text-center">
|
|
||||||
<h4 className="font-semibold text-gray-900 mb-2">Live Chat</h4>
|
|
||||||
<p className="text-gray-600 text-sm mb-1">Available 24/7</p>
|
|
||||||
<Link href="/chat" className="text-green-600 font-medium hover:text-green-700">
|
|
||||||
Start Chat
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-gray-900 mb-3">Business Hours</h4>
|
|
||||||
<div className="text-gray-600 text-sm space-y-1">
|
|
||||||
<p>
|
|
||||||
<strong>Monday - Saturday:</strong>
|
|
||||||
<br />
|
|
||||||
10:00 AM - 6:00 PM JST
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<strong>Sunday:</strong>
|
|
||||||
<br />
|
|
||||||
Closed
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<footer className="bg-white/90 backdrop-blur-sm text-gray-900 py-8 border-t border-gray-200">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-gray-600 text-sm">
|
|
||||||
© 2025 Assist Solutions Corp. All Rights Reserved.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,2 +1,3 @@
|
|||||||
export * from "./components";
|
export * from "./components";
|
||||||
export * from "./hooks";
|
export * from "./hooks";
|
||||||
|
export * from "./views";
|
||||||
|
|||||||
@ -1,23 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useAuthStore } from "@/features/auth/services/auth.store";
|
import type { Activity, DashboardSummary } from "@customer-portal/domain";
|
||||||
import { useDashboardSummary } from "@/features/dashboard/hooks/useDashboard";
|
|
||||||
|
|
||||||
import type { Activity } from "@customer-portal/domain";
|
|
||||||
import {
|
import {
|
||||||
CreditCardIcon,
|
|
||||||
ServerIcon,
|
ServerIcon,
|
||||||
ChatBubbleLeftRightIcon,
|
ChatBubbleLeftRightIcon,
|
||||||
ExclamationTriangleIcon,
|
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
PlusIcon,
|
|
||||||
DocumentTextIcon,
|
DocumentTextIcon,
|
||||||
ArrowTrendingUpIcon,
|
ArrowTrendingUpIcon,
|
||||||
CalendarDaysIcon,
|
CalendarDaysIcon,
|
||||||
BellIcon,
|
|
||||||
ClipboardDocumentListIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
import {
|
import {
|
||||||
CreditCardIcon as CreditCardIconSolid,
|
CreditCardIcon as CreditCardIconSolid,
|
||||||
@ -26,15 +19,19 @@ import {
|
|||||||
ClipboardDocumentListIcon as ClipboardDocumentListIconSolid,
|
ClipboardDocumentListIcon as ClipboardDocumentListIconSolid,
|
||||||
} from "@heroicons/react/24/solid";
|
} from "@heroicons/react/24/solid";
|
||||||
import { format, formatDistanceToNow } from "date-fns";
|
import { format, formatDistanceToNow } from "date-fns";
|
||||||
|
|
||||||
|
import { useAuthStore } from "@/features/auth/services/auth.store";
|
||||||
|
import { useDashboardSummary } from "@/features/dashboard/hooks";
|
||||||
import { StatCard, QuickAction, DashboardActivityItem } from "@/features/dashboard/components";
|
import { StatCard, QuickAction, DashboardActivityItem } from "@/features/dashboard/components";
|
||||||
import { LoadingSpinner } from "@/components/ui/loading-skeleton";
|
import { LoadingSpinner } from "@/components/ui/loading-skeleton";
|
||||||
import { ErrorState } from "@/components/ui/error-state";
|
import { ErrorState } from "@/components/ui/error-state";
|
||||||
import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain";
|
import { formatCurrency, getCurrencyLocale } from "@customer-portal/domain";
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export function DashboardView() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { user, isAuthenticated, loading: authLoading } = useAuthStore();
|
const { user, isAuthenticated, loading: authLoading } = useAuthStore();
|
||||||
const { data: summary, isLoading: summaryLoading, error } = useDashboardSummary();
|
const { data: summary, isLoading: summaryLoading, error } = useDashboardSummary();
|
||||||
|
const upcomingInvoice = summary?.nextInvoice ?? null;
|
||||||
|
|
||||||
const [paymentLoading, setPaymentLoading] = useState(false);
|
const [paymentLoading, setPaymentLoading] = useState(false);
|
||||||
const [paymentError, setPaymentError] = useState<string | null>(null);
|
const [paymentError, setPaymentError] = useState<string | null>(null);
|
||||||
@ -115,7 +112,7 @@ export default function DashboardPage() {
|
|||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-[var(--cp-space-2xl)] mb-[var(--cp-space-3xl)]">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-[var(--cp-space-2xl)] mb-[var(--cp-space-3xl)]">
|
||||||
<StatCard
|
<StatCard
|
||||||
title="Recent Orders"
|
title="Recent Orders"
|
||||||
value={((summary?.stats as Record<string, unknown>)?.recentOrders as number) || 0}
|
value={summary?.stats?.recentOrders ?? 0}
|
||||||
icon={ClipboardDocumentListIconSolid}
|
icon={ClipboardDocumentListIconSolid}
|
||||||
gradient="from-gray-500 to-gray-600"
|
gradient="from-gray-500 to-gray-600"
|
||||||
href="/orders"
|
href="/orders"
|
||||||
@ -157,7 +154,7 @@ export default function DashboardPage() {
|
|||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
<div className="lg:col-span-2 space-y-[var(--cp-space-3xl)]">
|
<div className="lg:col-span-2 space-y-[var(--cp-space-3xl)]">
|
||||||
{/* Upcoming Payment - compressed attention banner */}
|
{/* Upcoming Payment - compressed attention banner */}
|
||||||
{summary?.nextInvoice && (
|
{upcomingInvoice && (
|
||||||
<div
|
<div
|
||||||
id="attention"
|
id="attention"
|
||||||
className="bg-white rounded-xl border border-orange-200 shadow-sm p-4"
|
className="bg-white rounded-xl border border-orange-200 shadow-sm p-4"
|
||||||
@ -172,29 +169,28 @@ export default function DashboardPage() {
|
|||||||
<div className="flex flex-wrap items-center gap-2 text-sm text-gray-700">
|
<div className="flex flex-wrap items-center gap-2 text-sm text-gray-700">
|
||||||
<span className="font-semibold text-gray-900">Upcoming Payment</span>
|
<span className="font-semibold text-gray-900">Upcoming Payment</span>
|
||||||
<span className="text-gray-400">•</span>
|
<span className="text-gray-400">•</span>
|
||||||
<span>Invoice #{summary.nextInvoice.id}</span>
|
<span>Invoice #{upcomingInvoice.id}</span>
|
||||||
<span className="text-gray-400">•</span>
|
<span className="text-gray-400">•</span>
|
||||||
<span title={format(new Date(summary.nextInvoice.dueDate), "MMMM d, yyyy")}>
|
<span title={format(new Date(upcomingInvoice.dueDate), "MMMM d, yyyy")}>
|
||||||
Due{" "}
|
Due{" "}
|
||||||
{formatDistanceToNow(new Date(summary.nextInvoice.dueDate), {
|
{formatDistanceToNow(new Date(upcomingInvoice.dueDate), {
|
||||||
addSuffix: true,
|
addSuffix: true,
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-2xl font-bold text-gray-900">
|
<div className="mt-1 text-2xl font-bold text-gray-900">
|
||||||
{formatCurrency(summary.nextInvoice.amount, {
|
{formatCurrency(upcomingInvoice.amount, {
|
||||||
currency: summary.nextInvoice.currency || "JPY",
|
currency: upcomingInvoice.currency || "JPY",
|
||||||
locale: getCurrencyLocale(summary.nextInvoice.currency || "JPY"),
|
locale: getCurrencyLocale(upcomingInvoice.currency || "JPY"),
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1 text-xs text-gray-500">
|
<div className="mt-1 text-xs text-gray-500">
|
||||||
Exact due date:{" "}
|
Exact due date: {format(new Date(upcomingInvoice.dueDate), "MMMM d, yyyy")}
|
||||||
{format(new Date(summary.nextInvoice.dueDate), "MMMM d, yyyy")}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end gap-2">
|
<div className="flex flex-col items-end gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => handlePayNow(summary.nextInvoice!.id)}
|
onClick={() => handlePayNow(upcomingInvoice.id)}
|
||||||
disabled={paymentLoading}
|
disabled={paymentLoading}
|
||||||
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
className="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
@ -205,7 +201,7 @@ export default function DashboardPage() {
|
|||||||
{!paymentLoading && <ChevronRightIcon className="ml-2 h-4 w-4" />}
|
{!paymentLoading && <ChevronRightIcon className="ml-2 h-4 w-4" />}
|
||||||
</button>
|
</button>
|
||||||
<Link
|
<Link
|
||||||
href={`/billing/invoices/${summary.nextInvoice.id}`}
|
href={`/billing/invoices/${upcomingInvoice.id}`}
|
||||||
className="text-blue-600 hover:text-blue-700 font-medium text-sm"
|
className="text-blue-600 hover:text-blue-700 font-medium text-sm"
|
||||||
>
|
>
|
||||||
View invoice
|
View invoice
|
||||||
@ -276,12 +272,13 @@ export default function DashboardPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Helpers and small components (local to dashboard)
|
// Helpers and small components (local to dashboard)
|
||||||
function truncateName(name: string, len = 28) {
|
function TasksChip({
|
||||||
if (name.length <= len) return name;
|
summaryLoading,
|
||||||
return name.slice(0, Math.max(0, len - 1)) + "…";
|
summary,
|
||||||
}
|
}: {
|
||||||
|
summaryLoading: boolean;
|
||||||
function TasksChip({ summaryLoading, summary }: { summaryLoading: boolean; summary: any }) {
|
summary: DashboardSummary | undefined;
|
||||||
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
if (summaryLoading) return null;
|
if (summaryLoading) return null;
|
||||||
const tasks: Array<{ label: string; href: string }> = [];
|
const tasks: Array<{ label: string; href: string }> = [];
|
||||||
@ -339,7 +336,11 @@ function RecentActivityCard({
|
|||||||
<button
|
<button
|
||||||
key={opt.k}
|
key={opt.k}
|
||||||
onClick={() => setFilter(opt.k)}
|
onClick={() => setFilter(opt.k)}
|
||||||
className={`px-2.5 py-1 text-xs rounded-md font-medium ${filter === opt.k ? "bg-white text-gray-900 shadow" : "text-gray-600 hover:text-gray-900"}`}
|
className={`px-2.5 py-1 text-xs rounded-md font-medium ${
|
||||||
|
filter === opt.k
|
||||||
|
? "bg-white text-gray-900 shadow"
|
||||||
|
: "text-gray-600 hover:text-gray-900"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{opt.label}
|
{opt.label}
|
||||||
</button>
|
</button>
|
||||||
1
apps/portal/src/features/dashboard/views/index.ts
Normal file
1
apps/portal/src/features/dashboard/views/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from "./DashboardView";
|
||||||
@ -1,3 +1,5 @@
|
|||||||
export * as billing from "./billing";
|
export * as billing from "./billing";
|
||||||
export * as subscriptions from "./subscriptions";
|
export * as subscriptions from "./subscriptions";
|
||||||
export * as dashboard from "./dashboard";
|
export * as dashboard from "./dashboard";
|
||||||
|
export * as marketing from "./marketing";
|
||||||
|
export * as support from "./support";
|
||||||
|
|||||||
2
apps/portal/src/features/marketing/index.ts
Normal file
2
apps/portal/src/features/marketing/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./views/PublicLandingView";
|
||||||
|
export * from "./views/PublicLandingLoadingView";
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Skeleton } from "@/components/ui/loading-skeleton";
|
import { Skeleton } from "@/components/ui/loading-skeleton";
|
||||||
|
|
||||||
export default function RootLoading() {
|
export function PublicLandingLoadingView() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-blue-100 to-blue-900">
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-blue-100 to-blue-900">
|
||||||
<header className="bg-white/90 backdrop-blur-sm shadow-sm border-b border-blue-100">
|
<header className="bg-white/90 backdrop-blur-sm shadow-sm border-b border-blue-100">
|
||||||
@ -22,8 +22,11 @@ export default function RootLoading() {
|
|||||||
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8 space-y-8">
|
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8 space-y-8">
|
||||||
<Skeleton className="h-12 w-2/3" />
|
<Skeleton className="h-12 w-2/3" />
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
{Array.from({ length: 3 }).map((_, i) => (
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
<div key={i} className="bg-white rounded-2xl p-8 shadow-lg border border-gray-100 space-y-4">
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-white rounded-2xl p-8 shadow-lg border border-gray-100 space-y-4"
|
||||||
|
>
|
||||||
<Skeleton className="h-16 w-16 rounded-full mx-auto" />
|
<Skeleton className="h-16 w-16 rounded-full mx-auto" />
|
||||||
<Skeleton className="h-6 w-1/2 mx-auto" />
|
<Skeleton className="h-6 w-1/2 mx-auto" />
|
||||||
<Skeleton className="h-4 w-3/4 mx-auto" />
|
<Skeleton className="h-4 w-3/4 mx-auto" />
|
||||||
@ -36,5 +39,3 @@ export default function RootLoading() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
368
apps/portal/src/features/marketing/views/PublicLandingView.tsx
Normal file
368
apps/portal/src/features/marketing/views/PublicLandingView.tsx
Normal file
@ -0,0 +1,368 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Logo } from "@/components/ui/logo";
|
||||||
|
import {
|
||||||
|
ArrowPathIcon,
|
||||||
|
UserIcon,
|
||||||
|
SparklesIcon,
|
||||||
|
CreditCardIcon,
|
||||||
|
Cog6ToothIcon,
|
||||||
|
PhoneIcon,
|
||||||
|
ChartBarIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
export function PublicLandingView() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 via-blue-100 to-blue-900">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-white/90 backdrop-blur-sm shadow-sm border-b border-blue-100">
|
||||||
|
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
|
||||||
|
<div className="flex justify-between items-center h-16">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Logo size={40} />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-xl font-semibold text-gray-900">Assist Solutions</h1>
|
||||||
|
<p className="text-xs text-gray-500">Customer Portal</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<Link
|
||||||
|
href="/auth/login"
|
||||||
|
className="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition-colors duration-200 font-medium text-sm"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/support"
|
||||||
|
className="text-gray-600 hover:text-gray-900 px-4 py-2 text-sm transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Support
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="relative py-20 overflow-hidden">
|
||||||
|
{/* Abstract background elements */}
|
||||||
|
<div className="absolute inset-0">
|
||||||
|
<div className="absolute top-20 left-10 w-72 h-72 bg-blue-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"></div>
|
||||||
|
<div
|
||||||
|
className="absolute top-40 right-10 w-72 h-72 bg-indigo-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"
|
||||||
|
style={{ animationDelay: "2s" }}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className="absolute -bottom-8 left-20 w-72 h-72 bg-purple-300 rounded-full mix-blend-multiply filter blur-xl opacity-20 animate-pulse"
|
||||||
|
style={{ animationDelay: "4s" }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center">
|
||||||
|
<h1 className="text-5xl md:text-6xl font-bold text-gray-900 mb-[var(--cp-space-2xl)]">
|
||||||
|
New Assist Solutions Customer Portal
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<a
|
||||||
|
href="#portal-access"
|
||||||
|
className="bg-blue-600 text-white px-10 py-4 rounded-xl hover:bg-blue-700 transition-all duration-300 font-semibold text-lg shadow-lg hover:shadow-xl transform hover:-translate-y-1"
|
||||||
|
>
|
||||||
|
Get Started
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Customer Portal Access Section */}
|
||||||
|
<section id="portal-access" className="py-20">
|
||||||
|
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-4">Access Your Portal</h2>
|
||||||
|
<p className="text-lg text-gray-600">Choose the option that applies to you</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[var(--cp-space-3xl)] items-stretch">
|
||||||
|
{/* Existing Customers - Migration */}
|
||||||
|
<div className="bg-white rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-all duration-[var(--cp-transition-slow)] transform hover:-translate-y-2 border-[var(--cp-card-border)]">
|
||||||
|
<div className="text-center flex-1 flex flex-col">
|
||||||
|
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
|
||||||
|
<ArrowPathIcon className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 mb-4">Existing Customers</h3>
|
||||||
|
<p className="text-gray-600 mb-6 leading-relaxed">
|
||||||
|
Migrate to our new portal and enjoy enhanced security with modern interface.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-auto">
|
||||||
|
<Link
|
||||||
|
href="/auth/link-whmcs"
|
||||||
|
className="block bg-blue-600 text-white px-8 py-4 rounded-xl hover:bg-blue-700 transition-all duration-300 font-semibold text-lg mb-3 shadow-md hover:shadow-lg"
|
||||||
|
>
|
||||||
|
Migrate Your Account
|
||||||
|
</Link>
|
||||||
|
<p className="text-sm text-gray-500">Takes just a few minutes</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Portal Users */}
|
||||||
|
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 border border-gray-100">
|
||||||
|
<div className="text-center flex-1 flex flex-col">
|
||||||
|
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
|
||||||
|
<UserIcon className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 mb-4">Portal Users</h3>
|
||||||
|
<p className="text-gray-600 mb-6 leading-relaxed">
|
||||||
|
Sign in to access your dashboard and manage all your services efficiently.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-auto">
|
||||||
|
<Link
|
||||||
|
href="/auth/login"
|
||||||
|
className="block border-2 border-blue-600 text-blue-600 px-8 py-4 rounded-xl hover:bg-blue-600 hover:text-white transition-all duration-300 font-semibold text-lg mb-3"
|
||||||
|
>
|
||||||
|
Login to Portal
|
||||||
|
</Link>
|
||||||
|
<p className="text-sm text-gray-500">Secure access to your account</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* New Customers */}
|
||||||
|
<div className="bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl transition-all duration-300 transform hover:-translate-y-2 border border-gray-100">
|
||||||
|
<div className="text-center flex-1 flex flex-col">
|
||||||
|
<div className="flex items-center justify-center w-16 h-16 bg-blue-600 rounded-full mb-6 mx-auto">
|
||||||
|
<SparklesIcon className="w-8 h-8 text-white" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-2xl font-bold text-gray-900 mb-4">New Customers</h3>
|
||||||
|
<p className="text-gray-600 mb-6 leading-relaxed">
|
||||||
|
Create your account and access our full range of IT solutions and services.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-auto">
|
||||||
|
<Link
|
||||||
|
href="/auth/signup"
|
||||||
|
className="block border-2 border-blue-600 text-blue-600 px-8 py-4 rounded-xl hover:bg-blue-600 hover:text-white transition-all duration-300 font-semibold text-lg mb-3"
|
||||||
|
>
|
||||||
|
Create Account
|
||||||
|
</Link>
|
||||||
|
<p className="text-sm text-gray-500">Start your journey with us</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Portal Features Section */}
|
||||||
|
<section className="bg-white py-20">
|
||||||
|
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
|
||||||
|
<div className="text-center mb-16">
|
||||||
|
<h2 className="text-3xl font-bold text-gray-900 mb-4">Why Choose Assist Solutions</h2>
|
||||||
|
<p className="text-lg text-gray-600">
|
||||||
|
Modern tools to manage your IT services with confidence
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-[var(--cp-space-3xl)]">
|
||||||
|
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
|
||||||
|
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
|
||||||
|
<CreditCardIcon className="w-7 h-7 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-3">Automated Billing</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Transparent invoicing, automated payments, and flexible billing options tailored to
|
||||||
|
your business.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
|
||||||
|
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
|
||||||
|
<Cog6ToothIcon className="w-7 h-7 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-3">Service Management</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Control subscriptions, manage network services, and track usage from a single pane
|
||||||
|
of glass.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
|
||||||
|
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
|
||||||
|
<PhoneIcon className="w-7 h-7 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-3">Expert Support</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Dedicated support team with SLA-backed response times and proactive service
|
||||||
|
monitoring.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
|
||||||
|
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
|
||||||
|
<ChartBarIcon className="w-7 h-7 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-3">Actionable Insights</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Real-time analytics and reporting to help you optimize resource usage and forecast
|
||||||
|
demand.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
|
||||||
|
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
|
||||||
|
<ArrowPathIcon className="w-7 h-7 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-3">Seamless Migration</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Guided onboarding for WHMCS customers with automatic data migration and validation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white border border-gray-100 rounded-[var(--cp-card-radius-lg)] p-[var(--cp-space-3xl)] shadow-[var(--cp-card-shadow)] hover:shadow-[var(--cp-card-shadow-lg)] transition-shadow duration-300">
|
||||||
|
<div className="flex items-center justify-center w-14 h-14 bg-blue-100 rounded-full mb-6">
|
||||||
|
<SparklesIcon className="w-7 h-7 text-blue-600" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-semibold text-gray-900 mb-3">Future-Proof Platform</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
Built on modern infrastructure with continuous updates, security patches, and new
|
||||||
|
features.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section className="bg-gradient-to-br from-blue-900 via-blue-800 to-blue-700 py-20">
|
||||||
|
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-4xl font-bold text-white mb-6">
|
||||||
|
Ready to experience the new portal?
|
||||||
|
</h2>
|
||||||
|
<p className="text-blue-100 text-lg mb-8">
|
||||||
|
Join thousands of customers who trust Assist Solutions to keep their business
|
||||||
|
connected and secure.
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
|
<Link
|
||||||
|
href="/auth/signup"
|
||||||
|
className="inline-flex items-center justify-center px-8 py-3 border border-transparent text-base font-medium rounded-lg text-blue-600 bg-white hover:bg-blue-50 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Create an Account
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/auth/login"
|
||||||
|
className="inline-flex items-center justify-center px-8 py-3 border border-blue-300 text-base font-medium rounded-lg text-white bg-transparent hover:bg-blue-800 transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Portal Login
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white/10 backdrop-blur rounded-2xl p-8 border border-white/20">
|
||||||
|
<h3 className="text-xl font-semibold text-white mb-4">What’s included?</h3>
|
||||||
|
<ul className="space-y-4 text-blue-100">
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-blue-300"></span>
|
||||||
|
<span>Centralized service and subscription management dashboard</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-blue-300"></span>
|
||||||
|
<span>Automated billing with support for multiple payment methods</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-blue-300"></span>
|
||||||
|
<span>Priority access to our customer support specialists</span>
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-3">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-blue-300"></span>
|
||||||
|
<span>Insights and analytics to track usage and growth</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<footer className="bg-blue-950 text-blue-100 py-10">
|
||||||
|
<div className="max-w-[var(--cp-page-max-width)] mx-auto px-[var(--cp-page-padding)] sm:px-6 lg:px-8">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||||
|
<div>
|
||||||
|
<Logo size={32} />
|
||||||
|
<p className="mt-4 text-sm text-blue-200">
|
||||||
|
Delivering reliable IT solutions and support for businesses of all sizes.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-white uppercase tracking-wider">Portal</h3>
|
||||||
|
<ul className="mt-4 space-y-2 text-sm">
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/auth/login"
|
||||||
|
className="hover:text-white transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link
|
||||||
|
href="/auth/signup"
|
||||||
|
className="hover:text-white transition-colors duration-200"
|
||||||
|
>
|
||||||
|
Create Account
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/support" className="hover:text-white transition-colors duration-200">
|
||||||
|
Support Center
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-white uppercase tracking-wider">
|
||||||
|
Solutions
|
||||||
|
</h3>
|
||||||
|
<ul className="mt-4 space-y-2 text-sm">
|
||||||
|
<li>Network Services</li>
|
||||||
|
<li>Managed Security</li>
|
||||||
|
<li>Cloud Infrastructure</li>
|
||||||
|
<li>Professional Services</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-white uppercase tracking-wider">Contact</h3>
|
||||||
|
<ul className="mt-4 space-y-2 text-sm">
|
||||||
|
<li>support@assistsolutions.com</li>
|
||||||
|
<li>+1 (800) 555-0123</li>
|
||||||
|
<li>123 Innovation Drive, Suite 400</li>
|
||||||
|
<li>San Francisco, CA 94105</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-10 border-t border-blue-800 pt-6 text-sm text-blue-300 flex flex-col sm:flex-row justify-between gap-4">
|
||||||
|
<p>© {new Date().getFullYear()} Assist Solutions. All rights reserved.</p>
|
||||||
|
<div className="flex gap-6">
|
||||||
|
<Link href="#" className="hover:text-white transition-colors duration-200">
|
||||||
|
Privacy Policy
|
||||||
|
</Link>
|
||||||
|
<Link href="#" className="hover:text-white transition-colors duration-200">
|
||||||
|
Terms of Service
|
||||||
|
</Link>
|
||||||
|
<Link href="#" className="hover:text-white transition-colors duration-200">
|
||||||
|
Status
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,9 +1 @@
|
|||||||
/**
|
export * from "./views";
|
||||||
* Support Feature Module
|
|
||||||
* Customer support functionality including components, hooks, and services
|
|
||||||
*
|
|
||||||
* Note: This feature module is currently empty and ready for future implementation
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This feature module is not yet implemented
|
|
||||||
// Components, hooks, services, types, and utilities will be added as needed
|
|
||||||
|
|||||||
247
apps/portal/src/features/support/views/NewSupportCaseView.tsx
Normal file
247
apps/portal/src/features/support/views/NewSupportCaseView.tsx
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, type FormEvent } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import {
|
||||||
|
ArrowLeftIcon,
|
||||||
|
PaperAirplaneIcon,
|
||||||
|
ExclamationCircleIcon,
|
||||||
|
InformationCircleIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
import { logger } from "@customer-portal/logging";
|
||||||
|
|
||||||
|
export function NewSupportCaseView() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
subject: "",
|
||||||
|
category: "Technical",
|
||||||
|
priority: "Medium",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (event: FormEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
setIsSubmitting(true);
|
||||||
|
|
||||||
|
// Mock submission - would normally send to API
|
||||||
|
void (async () => {
|
||||||
|
try {
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||||
|
|
||||||
|
// Redirect to cases list with success message
|
||||||
|
router.push("/support/cases?created=true");
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ error }, "Error creating case");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (field: string, value: string) => {
|
||||||
|
setFormData(prev => ({
|
||||||
|
...prev,
|
||||||
|
[field]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFormValid = formData.subject.trim() && formData.description.trim();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<button
|
||||||
|
onClick={() => router.back()}
|
||||||
|
className="inline-flex items-center text-sm text-gray-500 hover:text-gray-700"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon className="h-4 w-4 mr-1" />
|
||||||
|
Back to Support
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-gray-900">Create Support Case</h1>
|
||||||
|
<p className="mt-1 text-sm text-gray-600">Get help from our support team</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Help Tips */}
|
||||||
|
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<InformationCircleIcon className="h-5 w-5 text-blue-400" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<h3 className="text-sm font-medium text-blue-800">Before creating a case</h3>
|
||||||
|
<div className="mt-2 text-sm text-blue-700">
|
||||||
|
<ul className="list-disc list-inside space-y-1">
|
||||||
|
<li>Check our knowledge base for common solutions</li>
|
||||||
|
<li>Include relevant error messages or screenshots</li>
|
||||||
|
<li>Provide detailed steps to reproduce the issue</li>
|
||||||
|
<li>Mention your service or subscription if applicable</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Form */}
|
||||||
|
<div className="bg-white shadow rounded-lg">
|
||||||
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
||||||
|
{/* Subject */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="subject" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Subject *
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="subject"
|
||||||
|
value={formData.subject}
|
||||||
|
onChange={event => handleInputChange("subject", event.target.value)}
|
||||||
|
placeholder="Brief description of your issue"
|
||||||
|
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category and Priority */}
|
||||||
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="category" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Category
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="category"
|
||||||
|
value={formData.category}
|
||||||
|
onChange={event => handleInputChange("category", event.target.value)}
|
||||||
|
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
>
|
||||||
|
<option value="Technical">Technical Support</option>
|
||||||
|
<option value="Billing">Billing Question</option>
|
||||||
|
<option value="General">General Inquiry</option>
|
||||||
|
<option value="Feature Request">Feature Request</option>
|
||||||
|
<option value="Bug Report">Bug Report</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="priority" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Priority
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="priority"
|
||||||
|
value={formData.priority}
|
||||||
|
onChange={event => handleInputChange("priority", event.target.value)}
|
||||||
|
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
>
|
||||||
|
<option value="Low">Low - General question</option>
|
||||||
|
<option value="Medium">Medium - Issue affecting work</option>
|
||||||
|
<option value="High">High - Service disruption</option>
|
||||||
|
<option value="Critical">Critical - Complete outage</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div>
|
||||||
|
<label htmlFor="description" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Description *
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
rows={6}
|
||||||
|
value={formData.description}
|
||||||
|
onChange={event => handleInputChange("description", event.target.value)}
|
||||||
|
placeholder="Please provide a detailed description of your issue, including any error messages and steps to reproduce the problem..."
|
||||||
|
className="block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p className="mt-2 text-xs text-gray-500">
|
||||||
|
The more details you provide, the faster we can help you.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Priority Warning */}
|
||||||
|
{formData.priority === "Critical" && (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||||
|
<div className="flex">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<ExclamationCircleIcon className="h-5 w-5 text-red-400" />
|
||||||
|
</div>
|
||||||
|
<div className="ml-3">
|
||||||
|
<h3 className="text-sm font-medium text-red-800">Critical Priority Selected</h3>
|
||||||
|
<div className="mt-2 text-sm text-red-700">
|
||||||
|
<p>
|
||||||
|
Critical priority should only be used for complete service outages. For
|
||||||
|
urgent issues that aren't complete outages, please use High priority.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex items-center justify-between pt-6 border-t border-gray-200">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => router.back()}
|
||||||
|
className="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={!isFormValid || isSubmitting}
|
||||||
|
className="inline-flex items-center px-6 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||||
|
Creating Case...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<PaperAirplaneIcon className="h-4 w-4 mr-2" />
|
||||||
|
Create Case
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Additional Help */}
|
||||||
|
<div className="mt-8 bg-gray-50 rounded-lg p-6">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">Need immediate help?</h3>
|
||||||
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-gray-900">Phone Support</h4>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
9:30-18:00 JST
|
||||||
|
<br />
|
||||||
|
<span className="font-medium text-blue-600">0120-660-470</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium text-gray-900">Knowledge Base</h4>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
Search our help articles for quick solutions
|
||||||
|
<br />
|
||||||
|
<Link href="/support/kb" className="font-medium text-blue-600 hover:text-blue-500">
|
||||||
|
Browse Knowledge Base
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
ChatBubbleLeftRightIcon,
|
ChatBubbleLeftRightIcon,
|
||||||
@ -28,7 +28,7 @@ interface SupportCase {
|
|||||||
assignedTo?: string;
|
assignedTo?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SupportCasesPage() {
|
export function SupportCasesView() {
|
||||||
const [cases, setCases] = useState<SupportCase[]>([]);
|
const [cases, setCases] = useState<SupportCase[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
@ -99,10 +99,12 @@ export default function SupportCasesPage() {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
setCases(mockCases);
|
setCases(mockCases);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Filter cases based on search, status, and priority
|
// Filter cases based on search, status, and priority
|
||||||
@ -228,8 +230,8 @@ export default function SupportCasesPage() {
|
|||||||
<dt className="text-sm font-medium text-gray-500 truncate">Open Cases</dt>
|
<dt className="text-sm font-medium text-gray-500 truncate">Open Cases</dt>
|
||||||
<dd className="text-lg font-medium text-gray-900">
|
<dd className="text-lg font-medium text-gray-900">
|
||||||
{
|
{
|
||||||
cases.filter(c =>
|
cases.filter(caseItem =>
|
||||||
["Open", "In Progress", "Waiting on Customer"].includes(c.status)
|
["Open", "In Progress", "Waiting on Customer"].includes(caseItem.status)
|
||||||
).length
|
).length
|
||||||
}
|
}
|
||||||
</dd>
|
</dd>
|
||||||
@ -249,7 +251,10 @@ export default function SupportCasesPage() {
|
|||||||
<dl>
|
<dl>
|
||||||
<dt className="text-sm font-medium text-gray-500 truncate">High Priority</dt>
|
<dt className="text-sm font-medium text-gray-500 truncate">High Priority</dt>
|
||||||
<dd className="text-lg font-medium text-gray-900">
|
<dd className="text-lg font-medium text-gray-900">
|
||||||
{cases.filter(c => ["High", "Critical"].includes(c.priority)).length}
|
{
|
||||||
|
cases.filter(caseItem => ["High", "Critical"].includes(caseItem.priority))
|
||||||
|
.length
|
||||||
|
}
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
@ -267,7 +272,10 @@ export default function SupportCasesPage() {
|
|||||||
<dl>
|
<dl>
|
||||||
<dt className="text-sm font-medium text-gray-500 truncate">Resolved</dt>
|
<dt className="text-sm font-medium text-gray-500 truncate">Resolved</dt>
|
||||||
<dd className="text-lg font-medium text-gray-900">
|
<dd className="text-lg font-medium text-gray-900">
|
||||||
{cases.filter(c => ["Resolved", "Closed"].includes(c.status)).length}
|
{
|
||||||
|
cases.filter(caseItem => ["Resolved", "Closed"].includes(caseItem.status))
|
||||||
|
.length
|
||||||
|
}
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
@ -289,7 +297,7 @@ export default function SupportCasesPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Search cases..."
|
placeholder="Search cases..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
onChange={event => setSearchTerm(event.target.value)}
|
||||||
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -298,7 +306,7 @@ export default function SupportCasesPage() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<select
|
<select
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={e => setStatusFilter(e.target.value)}
|
onChange={event => setStatusFilter(event.target.value)}
|
||||||
className="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
className="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
||||||
>
|
>
|
||||||
<option value="all">All Statuses</option>
|
<option value="all">All Statuses</option>
|
||||||
@ -314,7 +322,7 @@ export default function SupportCasesPage() {
|
|||||||
<div className="relative">
|
<div className="relative">
|
||||||
<select
|
<select
|
||||||
value={priorityFilter}
|
value={priorityFilter}
|
||||||
onChange={e => setPriorityFilter(e.target.value)}
|
onChange={event => setPriorityFilter(event.target.value)}
|
||||||
className="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
className="block w-full px-3 py-2 border border-gray-300 rounded-md leading-5 bg-white focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500"
|
||||||
>
|
>
|
||||||
<option value="all">All Priorities</option>
|
<option value="all">All Priorities</option>
|
||||||
2
apps/portal/src/features/support/views/index.ts
Normal file
2
apps/portal/src/features/support/views/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./NewSupportCaseView";
|
||||||
|
export * from "./SupportCasesView";
|
||||||
@ -15,6 +15,12 @@
|
|||||||
// Path mappings
|
// Path mappings
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"],
|
"@/*": ["./src/*"],
|
||||||
|
"@/components/*": ["./src/components/*"],
|
||||||
|
"@/core/*": ["./src/core/*"],
|
||||||
|
"@/features/*": ["./src/features/*"],
|
||||||
|
"@/shared/*": ["./src/shared/*"],
|
||||||
|
"@/styles/*": ["./src/styles/*"],
|
||||||
|
"@/types/*": ["./src/types/*"],
|
||||||
},
|
},
|
||||||
// Enforce TS-only in portal and keep strict mode explicit (inherits from root)
|
// Enforce TS-only in portal and keep strict mode explicit (inherits from root)
|
||||||
"allowJs": false,
|
"allowJs": false,
|
||||||
|
|||||||
@ -30,7 +30,7 @@ apps/portal/src/features/service-management/
|
|||||||
|
|
||||||
## Integration
|
## Integration
|
||||||
|
|
||||||
- Entry point: `apps/portal/src/app/(portal)/subscriptions/[id]/page.tsx` renders `ServiceManagementSection`
|
- Entry point: `apps/portal/src/app/(authenticated)/subscriptions/[id]/page.tsx` renders `ServiceManagementSection`
|
||||||
- Detection: SIM availability is inferred from `subscription.productName` including `sim` (case-insensitive)
|
- Detection: SIM availability is inferred from `subscription.productName` including `sim` (case-insensitive)
|
||||||
|
|
||||||
## Future Expansion
|
## Future Expansion
|
||||||
|
|||||||
@ -99,10 +99,10 @@ export default [
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
// Prevent importing the DashboardLayout directly in (portal) pages.
|
// Prevent importing the DashboardLayout directly in (authenticated) pages.
|
||||||
// Pages should rely on the shared route-group layout at (portal)/layout.tsx.
|
// Pages should rely on the shared route-group layout at (authenticated)/layout.tsx.
|
||||||
{
|
{
|
||||||
files: ["apps/portal/src/app/(portal)/**/*.{ts,tsx}"],
|
files: ["apps/portal/src/app/(authenticated)/**/*.{ts,tsx}"],
|
||||||
rules: {
|
rules: {
|
||||||
"no-restricted-imports": [
|
"no-restricted-imports": [
|
||||||
"error",
|
"error",
|
||||||
@ -111,7 +111,7 @@ export default [
|
|||||||
{
|
{
|
||||||
group: ["@/components/layout/dashboard-layout"],
|
group: ["@/components/layout/dashboard-layout"],
|
||||||
message:
|
message:
|
||||||
"Use the shared (portal)/layout.tsx instead of importing DashboardLayout in pages.",
|
"Use the shared (authenticated)/layout.tsx instead of importing DashboardLayout in pages.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@ -133,14 +133,14 @@ export default [
|
|||||||
},
|
},
|
||||||
// Allow the shared layout file itself to import the layout component
|
// Allow the shared layout file itself to import the layout component
|
||||||
{
|
{
|
||||||
files: ["apps/portal/src/app/(portal)/layout.tsx"],
|
files: ["apps/portal/src/app/(authenticated)/layout.tsx"],
|
||||||
rules: {
|
rules: {
|
||||||
"no-restricted-imports": "off",
|
"no-restricted-imports": "off",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
// Allow controlled window.location usage for invoice SSO download
|
// Allow controlled window.location usage for invoice SSO download
|
||||||
{
|
{
|
||||||
files: ["apps/portal/src/app/(portal)/billing/invoices/[id]/page.tsx"],
|
files: ["apps/portal/src/app/(authenticated)/billing/invoices/[id]/page.tsx"],
|
||||||
rules: {
|
rules: {
|
||||||
"no-restricted-syntax": "off",
|
"no-restricted-syntax": "off",
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user