On this page
Demo: Modular Pattern
Date: 2025-27-01
Status: accepted
Project Structure
src/
pages/
login/
components/
LoginForm.tsx
LoginHeader.tsx
hooks/
useLogin.ts
types/
index.ts
ui/page.tsx
dashboard/
components/
DashboardHeader.tsx
StatsCard.tsx
UserProfile.tsx
hooks/
useDashboard.ts
useUserStats.ts
types/
index.ts
ui/page.tsx
shared/
ui/ # Facade for Core UI
index.ts
core-ui/ # Internal UI-kit (Atomic Design)
atoms/
Button.tsx
Input.tsx
Card.tsx
Avatar.tsx
molecules/
FormField.tsx
StatCard.tsx
organisms/
Modal.tsx
Navigation.tsx
index.ts
Core UI (Atomic Design)
core-ui/atoms/Button.tsx
import React from "react";
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
variant?: "primary" | "secondary" | "danger";
size?: "sm" | "md" | "lg";
};
export const Button: React.FC<ButtonProps> = ({
variant = "primary",
size = "md",
...props
}) => {
const base = "font-medium rounded transition-colors";
const sizes = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2",
lg: "px-6 py-3 text-lg"
};
const variants = {
primary: "bg-blue-600 text-white hover:bg-blue-700",
secondary: "bg-gray-200 text-gray-800 hover:bg-gray-300",
danger: "bg-red-600 text-white hover:bg-red-700"
};
return (
<button
className={`${base} ${sizes[size]} ${variants[variant]}`}
{...props}
/>
);
};core-ui/atoms/Input.tsx
import React from "react";
type InputProps = React.InputHTMLAttributes<HTMLInputElement> & {
error?: boolean;
};
export const Input: React.FC<InputProps> = ({ error, ...props }) => {
const baseClasses = "border rounded px-3 py-2 w-full focus:outline-none focus:ring";
const errorClasses = error
? "border-red-300 focus:ring-red-400"
: "border-gray-300 focus:ring-blue-400";
return (
<input
{...props}
className={`${baseClasses} ${errorClasses}`}
/>
);
};core-ui/atoms/Card.tsx
import React from "react";
type CardProps = {
children: React.ReactNode;
className?: string;
};
export const Card: React.FC<CardProps> = ({ children, className = "" }) => {
return (
<div className={`bg-white shadow rounded-lg ${className}`}>
{children}
</div>
);
};core-ui/atoms/Avatar.tsx
import React from "react";
type AvatarProps = {
src?: string;
alt?: string;
name?: string;
size?: "sm" | "md" | "lg";
};
export const Avatar: React.FC<AvatarProps> = ({
src,
alt,
name,
size = "md"
}) => {
const sizes = {
sm: "w-8 h-8 text-sm",
md: "w-10 h-10",
lg: "w-16 h-16 text-lg"
};
const initials = name?.split(' ').map(n => n[0]).join('').toUpperCase();
return (
<div className={`${sizes[size]} rounded-full bg-gray-300 flex items-center justify-center overflow-hidden`}>
{src ? (
<img src={src} alt={alt || name} className="w-full h-full object-cover" />
) : (
<span className="text-gray-600 font-medium">{initials}</span>
)}
</div>
);
};core-ui/molecules/FormField.tsx
import React from "react";
type FormFieldProps = {
label: string;
error?: string;
children: React.ReactNode;
};
export const FormField: React.FC<FormFieldProps> = ({ label, error, children }) => (
<label className="block mb-4">
<span className="text-gray-700 mb-1 block font-medium">{label}</span>
{children}
{error && <span className="text-red-500 text-sm mt-1 block">{error}</span>}
</label>
);core-ui/molecules/StatCard.tsx
import React from "react";
import { Card } from "../atoms/Card";
type StatCardProps = {
title: string;
value: string | number;
change?: string;
trend?: "up" | "down" | "neutral";
};
export const StatCard: React.FC<StatCardProps> = ({
title,
value,
change,
trend = "neutral"
}) => {
const trendColors = {
up: "text-green-600",
down: "text-red-600",
neutral: "text-gray-600"
};
return (
<Card className="p-6">
<h3 className="text-sm font-medium text-gray-500 mb-2">{title}</h3>
<div className="text-2xl font-bold text-gray-900 mb-1">{value}</div>
{change && (
<div className={`text-sm ${trendColors[trend]}`}>
{trend === "up" && "↗"} {trend === "down" && "↘"} {change}
</div>
)}
</Card>
);
};core-ui/organisms/Navigation.tsx
import React from "react";
import { Button } from "../atoms/Button";
import { Avatar } from "../atoms/Avatar";
type NavigationProps = {
user?: {
name: string;
avatar?: string;
};
onLogout?: () => void;
};
export const Navigation: React.FC<NavigationProps> = ({ user, onLogout }) => {
return (
<nav className="bg-white shadow-sm border-b">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex items-center">
<h1 className="text-xl font-bold text-gray-900">Dashboard</h1>
</div>
<div className="flex items-center space-x-4">
{user && (
<>
<Avatar src={user.avatar} name={user.name} size="sm" />
<span className="text-gray-700">{user.name}</span>
<Button variant="secondary" size="sm" onClick={onLogout}>
Logout
</Button>
</>
)}
</div>
</div>
</div>
</nav>
);
};core-ui/index.ts
export { Button } from "./atoms/Button";
export { Input } from "./atoms/Input";
export { Card } from "./atoms/Card";
export { Avatar } from "./atoms/Avatar";
export { FormField } from "./molecules/FormField";
export { StatCard } from "./molecules/StatCard";
export { Navigation } from "./organisms/Navigation";Shared Layer (Facade)
shared/ui/index.ts
// Facade for Core UI — upper layers should not import directly from core-ui
export {
Button,
Input,
Card,
Avatar,
FormField,
StatCard,
Navigation
} from "core-ui";Page Module: Login
pages/login/types/index.ts
export interface LoginFormData {
email: string;
password: string;
}
export interface User {
id: number;
name: string;
email: string;
avatar?: string;
}
export interface LoginResponse {
user: User;
token: string;
}pages/login/hooks/useLogin.ts
import { useState } from "react";
import { LoginFormData, LoginResponse } from "../types";
export function useLogin() {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const login = async (data: LoginFormData): Promise<LoginResponse | null> => {
setLoading(true);
setError(null);
try {
// Simulate API request
await new Promise((res) => setTimeout(res, 1000));
// Simulate validation
if (data.email === "admin@example.com" && data.password === "password") {
const response: LoginResponse = {
user: {
id: 1,
name: "John Doe",
email: data.email,
avatar: "https://via.placeholder.com/40"
},
token: "fake-jwt-token"
};
return response;
} else {
throw new Error("Invalid credentials");
}
} catch (err) {
setError(err instanceof Error ? err.message : "Login failed");
return null;
} finally {
setLoading(false);
}
};
return { login, loading, error };
}pages/login/components/LoginHeader.tsx
import React from "react";
export const LoginHeader: React.FC = () => {
return (
<div className="text-center mb-8">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Welcome Back</h1>
<p className="text-gray-600">Sign in to your account to continue</p>
</div>
);
};pages/login/components/LoginForm.tsx
import React, { useState } from "react";
import { Button, Input, FormField } from "@/shared/ui";
import { useLogin } from "../hooks/useLogin";
import { LoginFormData } from "../types";
export const LoginForm: React.FC = () => {
const { login, loading, error } = useLogin();
const [formData, setFormData] = useState<LoginFormData>({
email: "",
password: ""
});
const handleChange = (field: keyof LoginFormData) => (
e: React.ChangeEvent<HTMLInputElement>
) => {
setFormData(prev => ({ ...prev, [field]: e.target.value }));
};
const onSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const result = await login(formData);
if (result) {
console.log("Login successful:", result);
// Handle successful login (redirect, store token, etc.)
}
};
return (
<form onSubmit={onSubmit} className="space-y-6">
<FormField label="Email Address" error={error}>
<Input
type="email"
value={formData.email}
onChange={handleChange("email")}
error={!!error}
placeholder="Enter your email"
/>
</FormField>
<FormField label="Password">
<Input
type="password"
value={formData.password}
onChange={handleChange("password")}
placeholder="Enter your password"
/>
</FormField>
<Button
type="submit"
disabled={loading}
className="w-full"
>
{loading ? "Signing in..." : "Sign In"}
</Button>
</form>
);
};pages/login/ui/page.tsx
import React from "react";
import { Card } from "@/shared/ui";
import { LoginHeader } from "../components/LoginHeader";
import { LoginForm } from "../components/LoginForm";
export default function LoginPage() {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full">
<LoginHeader />
<Card className="p-8">
<LoginForm />
</Card>
</div>
</div>
);
}Page Module: Dashboard
pages/dashboard/types/index.ts
export interface UserStats {
totalUsers: number;
activeUsers: number;
newUsers: number;
userGrowth: string;
}
export interface DashboardData {
user: {
id: number;
name: string;
email: string;
avatar?: string;
};
stats: UserStats;
}pages/dashboard/hooks/useDashboard.ts
import { useState, useEffect } from "react";
import { DashboardData } from "../types";
export function useDashboard() {
const [data, setData] = useState<DashboardData | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchDashboardData = async () => {
setLoading(true);
// Simulate API request
await new Promise((res) => setTimeout(res, 1000));
const mockData: DashboardData = {
user: {
id: 1,
name: "John Doe",
email: "john@example.com",
avatar: "https://via.placeholder.com/40"
},
stats: {
totalUsers: 1250,
activeUsers: 890,
newUsers: 45,
userGrowth: "+12.5%"
}
};
setData(mockData);
setLoading(false);
};
fetchDashboardData();
}, []);
return { data, loading };
}pages/dashboard/hooks/useUserStats.ts
import { useState, useEffect } from "react";
import { UserStats } from "../types";
export function useUserStats() {
const [stats, setStats] = useState<UserStats | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchStats = async () => {
setLoading(true);
// Simulate API request
await new Promise((res) => setTimeout(res, 800));
const mockStats: UserStats = {
totalUsers: 1250,
activeUsers: 890,
newUsers: 45,
userGrowth: "+12.5%"
};
setStats(mockStats);
setLoading(false);
};
fetchStats();
}, []);
return { stats, loading };
}pages/dashboard/components/DashboardHeader.tsx
import React from "react";
import { Navigation } from "@/shared/ui";
type DashboardHeaderProps = {
user: {
name: string;
avatar?: string;
};
onLogout?: () => void;
};
export const DashboardHeader: React.FC<DashboardHeaderProps> = ({
user,
onLogout
}) => {
return (
<Navigation
user={user}
onLogout={onLogout}
/>
);
};pages/dashboard/components/StatsCard.tsx
import React from "react";
import { StatCard } from "@/shared/ui";
type StatsCardProps = {
title: string;
value: number;
change?: string;
trend?: "up" | "down" | "neutral";
};
export const StatsCard: React.FC<StatsCardProps> = ({
title,
value,
change,
trend
}) => {
return (
<StatCard
title={title}
value={value.toLocaleString()}
change={change}
trend={trend}
/>
);
};pages/dashboard/components/UserProfile.tsx
import React from "react";
import { Card, Avatar } from "@/shared/ui";
type UserProfileProps = {
user: {
name: string;
email: string;
avatar?: string;
};
};
export const UserProfile: React.FC<UserProfileProps> = ({ user }) => {
return (
<Card className="p-6">
<div className="flex items-center space-x-4">
<Avatar src={user.avatar} name={user.name} size="lg" />
<div>
<h3 className="text-lg font-semibold text-gray-900">{user.name}</h3>
<p className="text-gray-600">{user.email}</p>
</div>
</div>
</Card>
);
};pages/dashboard/ui/page.tsx
import React from "react";
import { useDashboard } from "../hooks/useDashboard";
import { DashboardHeader } from "../components/DashboardHeader";
import { StatsCard } from "../components/StatsCard";
import { UserProfile } from "../components/UserProfile";
export default function DashboardPage() {
const { data, loading } = useDashboard();
const handleLogout = () => {
console.log("Logging out...");
// Handle logout logic
};
if (loading) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-lg text-gray-600">Loading dashboard...</div>
</div>
);
}
if (!data) {
return (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="text-lg text-red-600">Failed to load dashboard</div>
</div>
);
}
return (
<div className="min-h-screen bg-gray-50">
<DashboardHeader user={data.user} onLogout={handleLogout} />
<main className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div className="px-4 py-6 sm:px-0">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<StatsCard
title="Total Users"
value={data.stats.totalUsers}
change="+5.2%"
trend="up"
/>
<StatsCard
title="Active Users"
value={data.stats.activeUsers}
change="+2.1%"
trend="up"
/>
<StatsCard
title="New Users"
value={data.stats.newUsers}
change="+12.5%"
trend="up"
/>
<StatsCard
title="Growth Rate"
value={data.stats.userGrowth}
trend="up"
/>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2">
<Card className="p-6">
<h2 className="text-xl font-semibold mb-4">Dashboard Overview</h2>
<p className="text-gray-600">
Welcome to your dashboard! Here you can see all the important metrics and information.
</p>
</Card>
</div>
<div>
<UserProfile user={data.user} />
</div>
</div>
</div>
</main>
</div>
);
}Summary
Modular Pattern Benefits:
- Page-based modules: Each page (
/login,/dashboard) is a self-contained module with its own components, hooks, and types - Clear separation: Components, hooks, and types are organized within each page module
- Reusable Core UI: Atomic Design principles with atoms, molecules, and organisms
- Shared facade: Core UI is accessed through a shared facade to maintain dependency rules
- Independent modules: Each page module can be developed, tested, and maintained independently
- Scalable structure: Easy to add new pages without affecting existing modules