A multi-tenant SaaS dashboard built for Pakistani law firms and legal departments. The frontend is a fully integrated React application that connects to the LTS-API backend, providing complete case management, hearing tracking, document storage, and more.
- Overview
- Tech Stack
- Architecture
- Project Structure
- Features
- Getting Started
- Environment Variables
- How a Feature Works
- State Management
- Routing
- API Integration
- Forms & Validation
- Contributing
LTS-Client is the authenticated dashboard for LTS. It is a separate application from the public landing page and communicates entirely with the LTS-API backend via REST API calls using JWT authentication.
www.lts.com → Landing Page (Next.js — separate repo)
app.lts.com → This repo (React dashboard)
api.lts.com → LTS-API (ASP.NET 10)
| Category | Technology |
|---|---|
| Framework | React 18 + TypeScript |
| Build Tool | Vite |
| Data Fetching | TanStack Query (React Query) |
| HTTP Client | Axios |
| Global State | Zustand |
| Forms | React Hook Form + Zod |
| Styling | Bootstrap + Custom CSS |
| Animations | Framer Motion |
| Routing | React Router DOM v6 |
| Validation Schema | Zod + @hookform/resolvers |
LTS-Client follows a Feature-Based Architecture — code is organized by feature, not by type. This mirrors the backend's Vertical Slice architecture.
Every feature is self-contained. It owns its own API calls, hooks, components, and types. No feature reaches into another feature's folder.
Component
↓
useQuery / useMutation hook (React Query)
↓
API function (Axios)
↓
LTS-API Backend
↓
Response cached by React Query
↓
Component re-renders with data
| Backend | Frontend |
|---|---|
| Query Handler (read) | useQuery hook |
| Command Handler (write) | useMutation hook |
| MediatR dispatcher | React Query's queryClient |
IMemoryCache |
React Query cache (5 min stale time) |
src/
├── features/
│ ├── cases/
│ │ ├── api/
│ │ │ └── casesApi.ts # All Axios calls for Cases
│ │ ├── hooks/
│ │ │ ├── useCases.ts # useQuery — fetch cases list
│ │ │ ├── useCaseById.ts # useQuery — fetch single case
│ │ │ └── useCreateCase.ts # useMutation — create case
│ │ ├── components/
│ │ │ ├── CasesPage.tsx # Main page component
│ │ │ └── CaseForm.tsx # Create/Edit form
│ │ └── types/
│ │ └── case.types.ts # TypeScript interfaces
│ ├── courts/
│ ├── departments/
│ ├── petitioners/
│ ├── followup/
│ ├── documents/
│ ├── bench/
│ ├── auth/
│ ├── reports/
│ └── superAdmin/
│
├── shared/
│ ├── components/
│ │ ├── Layout.tsx # Sidebar + Navbar wrapper
│ │ ├── Sidebar.tsx
│ │ ├── Navbar.tsx
│ │ └── ProtectedRoute.tsx
│ └── ui/
│ └── (reusable UI components)
│
├── lib/
│ ├── axiosInstance.ts # Axios setup + JWT interceptor
│ └── queryClient.ts # React Query global config
│
├── store/
│ └── authStore.ts # Zustand auth state
│
├── router/
│ └── index.tsx # All app routes
│
├── styles/
│ └── (global CSS files)
│
├── App.tsx
└── main.tsx
| Feature | Description |
|---|---|
| Auth | Login, logout, OTP verification, JWT token management |
| Cases | Full CRUD, search, status management, case details |
| Courts | Court listing and management |
| Departments | Department listing and management |
| Petitioners | Petitioner profiles linked to cases |
| Followup / Hearings | Hearing dates, interim orders, decisions |
| Documents | File upload and download per case |
| Bench | Judge assignment per case |
| Reports | Department-wise and court-wise case reports |
| Dashboard | Stats overview, upcoming hearings, recent cases |
| SuperAdmin | Organization management, plan assignment, payment verification |
- Node.js 20+
- npm or yarn
- LTS-API running locally or accessible via URL
git clone https://github.com/fayaz921/LTS-Client.git
cd LTS-Clientnpm installCreate a .env file in the root of the project:
VITE_API_BASE_URL=http://localhost:5245/apinpm run devApp will be available at http://localhost:5173
npm run build| Variable | Description | Example |
|---|---|---|
VITE_API_BASE_URL |
Base URL of the LTS-API backend | http://localhost:5245/api |
All Vite environment variables must start with
VITE_prefix to be accessible in the app viaimport.meta.env.VITE_*
Never commit your
.envfile. It is already in.gitignore.
Every feature follows the same 4-folder pattern. Here is the complete flow for the Cases feature:
All Axios HTTP calls for this feature. No business logic here.
import api from '../../lib/axiosInstance'
import { CreateCaseDto, CaseDto } from '../types/case.types'
export const getAllCases = async (): Promise<CaseDto[]> => {
const { data } = await api.get('/Cases/GetAllCases')
return data.data
}
export const createCase = async (dto: CreateCaseDto): Promise<string> => {
const { data } = await api.post('/Cases/CreateCase', dto)
return data.data
}React Query hooks that wrap the API calls. Handles caching, loading, and error states automatically.
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { getAllCases, createCase } from '../api/casesApi'
// GET — useQuery
export const useCases = () => {
return useQuery({
queryKey: ['cases'],
queryFn: getAllCases,
})
}
// POST — useMutation
export const useCreateCase = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: createCase,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['cases'] })
},
})
}Consumes the hook. No direct API calls ever inside components.
const CasesPage = () => {
const { data: cases, isLoading, isError } = useCases()
if (isLoading) return <div>Loading...</div>
if (isError) return <div>Something went wrong</div>
return (
<div>
{cases?.map(c => <div key={c.id}>{c.title}</div>)}
</div>
)
}TypeScript interfaces that match the backend DTOs exactly.
export interface CaseDto {
id: string
caseNo: string
title: string
subject: string
status: CaseStatus
dateInstitution: string
}
export interface CreateCaseDto {
caseNo: string
title: string
subject: string
detail: string
courtId: string
departmentId: string
}
export enum CaseStatus {
Pending = 0,
Finalized = 1,
}Zustand manages global authentication state. No prop drilling needed.
// store/authStore.ts
import { create } from 'zustand'
export const useAuthStore = create((set) => ({
user: null,
token: null,
isAuthenticated: false,
setAuth: (user, token) => {
localStorage.setItem('token', token)
set({ user, token, isAuthenticated: true })
},
logout: () => {
localStorage.removeItem('token')
set({ user: null, token: null, isAuthenticated: false })
},
}))// After successful login
const { setAuth } = useAuthStore()
setAuth(response.data.user, response.data.token)
// In any component — read current user
const { user, isAuthenticated } = useAuthStore()
// Logout
const { logout } = useAuthStore()All routes are defined in router/index.tsx. Protected routes live under /app and require authentication.
export const router = createBrowserRouter([
{ path: '/', element: <LandingPage /> },
{ path: '/login', element: <LoginPage /> },
{
path: '/app',
element: <Layout />, // Sidebar + Navbar wrapper
children: [
{ path: 'dashboard', element: <DashboardPage /> },
{ path: 'cases', element: <CasesPage /> },
{ path: 'courts', element: <CourtsPage /> },
{ path: 'departments', element: <DepartmentsPage /> },
{ path: 'followup', element: <FollowupPage /> },
{ path: 'documents', element: <DocumentsPage /> },
{ path: 'bench', element: <BenchPage /> },
{ path: 'reports', element: <ReportsPage /> },
],
},
])Automatically attaches JWT token to every request and handles 401 expiry redirects.
import axios from 'axios'
const api = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
})
// Attach JWT token to every request
api.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
// Handle 401 — redirect to login on token expiry
api.interceptors.response.use(
response => response,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
window.location.href = '/login'
}
return Promise.reject(error)
}
)
export default apiReact Hook Form + Zod handle all form state and validation — mirroring FluentValidation on the backend.
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const schema = z.object({
caseNo: z.string().min(1, 'Case number is required').max(50),
title: z.string().min(1, 'Title is required').max(200),
subject: z.string().min(1, 'Subject is required'),
})
type FormData = z.infer<typeof schema>
const CaseForm = () => {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(schema),
})
const onSubmit = (data: FormData) => {
// call mutation here
}
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('caseNo')} />
{errors.caseNo && <span>{errors.caseNo.message}</span>}
</form>
)
}This is a private team project. Branch and PR rules:
- Never push directly to
main - Create a feature branch:
Feature/YourFeatureName - Open a Pull Request — CI must pass before merge
- PR must be reviewed and approved by the Tech Lead before merging
Feature/CasesPage
Feature/FollowupModule
Feature/SuperAdminDashboard
| Rule | Description |
|---|---|
| Feature Rule | Every feature owns its own api/, hooks/, components/, types/ folders. Never reach into another feature's folder |
| API Rule | All Axios calls go in that feature's api/ folder only. Never call Axios directly inside a component |
| Hook Rule | Use useQuery for GET requests. Use useMutation for POST/PUT/DELETE. Never use useEffect for API calls |
| Type Rule | Every API response must have a TypeScript interface in types/. Never use any type |
| Route Rule | All protected routes go inside /app children. Public routes go at root level |
| Auth Rule | After login call setAuth(). To get current user call useAuthStore(). Never store token manually |
| Component Rule | Keep components focused. If a component exceeds 150 lines, split it into smaller ones |
| Naming Rule | PascalCase for components (CasesPage.tsx), camelCase for hooks (useCases.ts) and API files (casesApi.ts) |
| CSS Rule | All dashboard styles use Bootstrap classes first. Custom CSS only when Bootstrap cannot do it |
| Repo | Description |
|---|---|
| LTS-API | ASP.NET 10 Web API backend |
| LTS-Landing | Next.js public landing page (coming soon) |
Litigation Tracking System — Built for Pakistani Law Firms