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