본문 바로가기

교육

React 테마 디자인 추가

반응형

/utils/themeConfig.ts

// Theme Types
export type ThemeType = 'government' | 'enterprise' | 'university' | 'global';

// Theme Configuration
export interface ThemeConfig {
  id: ThemeType;
  name: string;
  colors: {
    primary: string;
    secondary: string;
    accent: string;
    background: string;
    surface: string;
    text: string;
    textSecondary: string;
    border: string;
    success: string;
    warning: string;
    error: string;
    info: string;
  };
  gradients: {
    primary: string;
    secondary: string;
    header: string;
    sidebar: string;
  };
  typography: {
    fontFamily: string;
    heading: string;
  };
  effects: {
    shadow: string;
    shadowHover: string;
    blur: string;
  };
  branding: {
    logo?: string;
    favicon?: string;
    headerStyle: 'professional' | 'modern' | 'gradient' | 'minimal';
  };
}

// Theme Presets
export const themePresets: Record<ThemeType, ThemeConfig> = {
  government: {
    id: 'government',
    name: 'Government',
    colors: {
      primary: '#1e40af', // Deep Blue
      secondary: '#0369a1', // Sky Blue
      accent: '#dc2626', // Red accent
      background: '#f8fafc',
      surface: '#ffffff',
      text: '#1e293b',
      textSecondary: '#64748b',
      border: '#cbd5e1',
      success: '#059669',
      warning: '#d97706',
      error: '#dc2626',
      info: '#0284c7',
    },
    gradients: {
      primary: 'linear-gradient(135deg, #1e40af 0%, #0369a1 100%)',
      secondary: 'linear-gradient(135deg, #0369a1 0%, #0ea5e9 100%)',
      header: 'linear-gradient(90deg, #1e3a8a 0%, #1e40af 50%, #0369a1 100%)',
      sidebar: 'linear-gradient(180deg, #1e40af 0%, #1e3a8a 100%)',
    },
    typography: {
      fontFamily: "'Inter', 'Noto Sans KR', sans-serif",
      heading: "'Roboto', 'Noto Sans KR', sans-serif",
    },
    effects: {
      shadow: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
      shadowHover: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
      blur: 'backdrop-filter: blur(8px)',
    },
    branding: {
      headerStyle: 'professional',
    },
  },
  enterprise: {
    id: 'enterprise',
    name: 'Enterprise',
    colors: {
      primary: '#0f172a', // Slate Dark
      secondary: '#334155', // Slate
      accent: '#f59e0b', // Amber
      background: '#f1f5f9',
      surface: '#ffffff',
      text: '#0f172a',
      textSecondary: '#475569',
      border: '#e2e8f0',
      success: '#10b981',
      warning: '#f59e0b',
      error: '#ef4444',
      info: '#3b82f6',
    },
    gradients: {
      primary: 'linear-gradient(135deg, #0f172a 0%, #334155 100%)',
      secondary: 'linear-gradient(135deg, #334155 0%, #475569 100%)',
      header: 'linear-gradient(90deg, #020617 0%, #0f172a 50%, #1e293b 100%)',
      sidebar: 'linear-gradient(180deg, #0f172a 0%, #020617 100%)',
    },
    typography: {
      fontFamily: "'Poppins', 'Noto Sans KR', sans-serif",
      heading: "'Montserrat', 'Noto Sans KR', sans-serif",
    },
    effects: {
      shadow: '0 4px 6px -1px rgba(0, 0, 0, 0.15), 0 2px 4px -1px rgba(0, 0, 0, 0.1)',
      shadowHover: '0 20px 25px -5px rgba(0, 0, 0, 0.15), 0 10px 10px -5px rgba(0, 0, 0, 0.1)',
      blur: 'backdrop-filter: blur(10px)',
    },
    branding: {
      headerStyle: 'modern',
    },
  },
  university: {
    id: 'university',
    name: 'University',
    colors: {
      primary: '#7c3aed', // Violet
      secondary: '#a855f7', // Purple
      accent: '#ec4899', // Pink
      background: '#faf5ff',
      surface: '#ffffff',
      text: '#3b0764',
      textSecondary: '#6b21a8',
      border: '#e9d5ff',
      success: '#22c55e',
      warning: '#eab308',
      error: '#f43f5e',
      info: '#8b5cf6',
    },
    gradients: {
      primary: 'linear-gradient(135deg, #7c3aed 0%, #a855f7 100%)',
      secondary: 'linear-gradient(135deg, #a855f7 0%, #ec4899 100%)',
      header: 'linear-gradient(90deg, #5b21b6 0%, #7c3aed 50%, #a855f7 100%)',
      sidebar: 'linear-gradient(180deg, #7c3aed 0%, #6d28d9 100%)',
    },
    typography: {
      fontFamily: "'Nunito', 'Noto Sans KR', sans-serif",
      heading: "'Quicksand', 'Noto Sans KR', sans-serif",
    },
    effects: {
      shadow: '0 4px 6px -1px rgba(124, 58, 237, 0.1), 0 2px 4px -1px rgba(124, 58, 237, 0.06)',
      shadowHover: '0 10px 15px -3px rgba(124, 58, 237, 0.2), 0 4px 6px -2px rgba(124, 58, 237, 0.1)',
      blur: 'backdrop-filter: blur(12px)',
    },
    branding: {
      headerStyle: 'gradient',
    },
  },
  global: {
    id: 'global',
    name: 'Global',
    colors: {
      primary: '#0891b2', // Cyan
      secondary: '#06b6d4', // Cyan Light
      accent: '#f97316', // Orange
      background: '#f0fdfa',
      surface: '#ffffff',
      text: '#134e4a',
      textSecondary: '#0f766e',
      border: '#99f6e4',
      success: '#14b8a6',
      warning: '#fb923c',
      error: '#f43f5e',
      info: '#0ea5e9',
    },
    gradients: {
      primary: 'linear-gradient(135deg, #0891b2 0%, #06b6d4 100%)',
      secondary: 'linear-gradient(135deg, #06b6d4 0%, #22d3ee 100%)',
      header: 'linear-gradient(90deg, #155e75 0%, #0891b2 50%, #06b6d4 100%)',
      sidebar: 'linear-gradient(180deg, #0891b2 0%, #0e7490 100%)',
    },
    typography: {
      fontFamily: "'Open Sans', 'Noto Sans KR', sans-serif",
      heading: "'Raleway', 'Noto Sans KR', sans-serif",
    },
    effects: {
      shadow: '0 4px 6px -1px rgba(8, 145, 178, 0.1), 0 2px 4px -1px rgba(8, 145, 178, 0.06)',
      shadowHover: '0 10px 15px -3px rgba(8, 145, 178, 0.2), 0 4px 6px -2px rgba(8, 145, 178, 0.1)',
      blur: 'backdrop-filter: blur(10px)',
    },
    branding: {
      headerStyle: 'minimal',
    },
  },
};

// Helper function to get theme
export const getTheme = (themeType: ThemeType): ThemeConfig => {
  return themePresets[themeType];
};

// Helper function to apply theme to CSS variables
export const applyTheme = (theme: ThemeConfig) => {
  const root = document.documentElement;
  
  // Apply colors
  Object.entries(theme.colors).forEach(([key, value]) => {
    root.style.setProperty(`--color-${key}`, value);
  });
  
  // Apply font family
  root.style.setProperty('--font-family', theme.typography.fontFamily);
  root.style.setProperty('--font-heading', theme.typography.heading);
};

 

 

/features/theme/themeSlice.ts

import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
import type { ThemeType, ThemeConfig } from '../../utils/themeConfig';
import { themePresets } from '../../utils/themeConfig';

interface ThemeState {
  currentTheme: ThemeType;
  config: ThemeConfig;
}

// Load theme from localStorage
const loadTheme = (): ThemeType => {
  const saved = localStorage.getItem('appTheme');
  return (saved as ThemeType) || 'enterprise';
};

const initialState: ThemeState = {
  currentTheme: loadTheme(),
  config: themePresets[loadTheme()],
};

const themeSlice = createSlice({
  name: 'theme',
  initialState,
  reducers: {
    setTheme: (state, action: PayloadAction<ThemeType>) => {
      state.currentTheme = action.payload;
      state.config = themePresets[action.payload];
      localStorage.setItem('appTheme', action.payload);
    },
  },
});

export const { setTheme } = themeSlice.actions;
export default themeSlice.reducer;

 

/components/Header/Header.tsx

import { useState } from 'react';
import { FaBars, FaBuilding, FaUser, FaPalette } from 'react-icons/fa';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { toggleSidebar } from '../../features/ui/uiSlice';
import AffiliationModal from '../Modal/AffiliationModal';
import UserSettingsModal from '../Modal/UserSettingsModal';
import ThemeSelector from '../ThemeSelector/ThemeSelector';

const Header = () => {
  const dispatch = useAppDispatch();
  const { user, affiliation } = useAppSelector((state) => state.auth);
  const { config } = useAppSelector((state) => state.theme);
  const [showAffiliationModal, setShowAffiliationModal] = useState(false);
  const [showUserSettingsModal, setShowUserSettingsModal] = useState(false);
  const [showThemeSelector, setShowThemeSelector] = useState(false);

  const handleToggleSidebar = () => {
    dispatch(toggleSidebar());
  };

  return (
    <>
      <header
        className="h-16 text-white shadow-2xl flex items-center justify-between px-6 fixed top-0 left-0 right-0 z-50 backdrop-blur-lg"
        style={{
          background: config.gradients.header,
          boxShadow: config.effects.shadowHover,
        }}
      >
        {/* Left: Hamburger Menu & Logo */}
        <div className="flex items-center gap-4">
          <button
            onClick={handleToggleSidebar}
            className="p-2.5 hover:bg-white/20 rounded-xl transition-all duration-300 transform hover:scale-110"
            aria-label="Toggle Sidebar"
          >
            <FaBars className="text-xl" />
          </button>
          <div className="flex items-center gap-3">
            <div className="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center backdrop-blur-md">
              <span className="text-2xl font-bold">W</span>
            </div>
            <div className="hidden md:block">
              <h1 className="text-lg font-bold tracking-tight">
                Warehouse Management
              </h1>
              <p className="text-xs opacity-80">Enterprise System</p>
            </div>
          </div>
        </div>

        {/* Right: Theme, Affiliation & User Info */}
        <div className="flex items-center gap-2">
          {/* Theme Selector Button */}
          <button
            onClick={() => setShowThemeSelector(true)}
            className="flex items-center gap-2 px-3 py-2 hover:bg-white/20 rounded-xl transition-all duration-300 backdrop-blur-sm"
            title="Change Theme"
          >
            <FaPalette className="text-lg" />
            <span className="text-sm hidden lg:block">Theme</span>
          </button>

          {/* Affiliation Info */}
          <button
            onClick={() => setShowAffiliationModal(true)}
            className="flex items-center gap-2 px-4 py-2 hover:bg-white/20 rounded-xl transition-all duration-300 backdrop-blur-sm border border-white/20"
          >
            <FaBuilding className="text-lg" />
            <div className="text-left hidden md:block">
              <div className="text-xs opacity-80">Company</div>
              <div className="text-sm font-semibold truncate max-w-[120px]">
                {affiliation?.companyName || 'N/A'}
              </div>
            </div>
            <div className="text-left hidden lg:block ml-2 pl-2 border-l border-white/30">
              <div className="text-xs opacity-80">Department</div>
              <div className="text-sm font-semibold truncate max-w-[120px]">
                {affiliation?.departmentName || 'N/A'}
              </div>
            </div>
          </button>

          {/* User Info */}
          <button
            onClick={() => setShowUserSettingsModal(true)}
            className="flex items-center gap-3 px-4 py-2 hover:bg-white/20 rounded-xl transition-all duration-300 backdrop-blur-sm border border-white/20"
          >
            <div className="w-9 h-9 bg-white/30 rounded-full flex items-center justify-center backdrop-blur-md">
              <FaUser className="text-sm" />
            </div>
            <div className="text-left hidden md:block">
              <div className="text-sm font-semibold">
                {user?.fullName || user?.username || 'User'}
              </div>
              <div className="text-xs opacity-80 truncate max-w-[150px]">
                {user?.email || ''}
              </div>
            </div>
          </button>
        </div>
      </header>

      {/* Modals */}
      <AffiliationModal
        isOpen={showAffiliationModal}
        onClose={() => setShowAffiliationModal(false)}
      />
      <UserSettingsModal
        isOpen={showUserSettingsModal}
        onClose={() => setShowUserSettingsModal(false)}
      />
      <ThemeSelector
        isOpen={showThemeSelector}
        onClose={() => setShowThemeSelector(false)}
      />
    </>
  );
};

export default Header;

 

/components/ThemeSelector/ThemeSelector.tsx

import Modal from '../Modal/Modal';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { setTheme } from '../../features/theme/themeSlice';
import { themePresets, type ThemeType } from '../../utils/themeConfig';
import {
  FaLandmark,
  FaBuilding,
  FaUniversity,
  FaGlobe,
  FaCheck,
} from 'react-icons/fa';

interface ThemeSelectorProps {
  isOpen: boolean;
  onClose: () => void;
}

const ThemeSelector = ({ isOpen, onClose }: ThemeSelectorProps) => {
  const dispatch = useAppDispatch();
  const { currentTheme } = useAppSelector((state) => state.theme);

  const themeIcons: Record<ThemeType, React.ReactNode> = {
    government: <FaLandmark className="text-3xl" />,
    enterprise: <FaBuilding className="text-3xl" />,
    university: <FaUniversity className="text-3xl" />,
    global: <FaGlobe className="text-3xl" />,
  };

  const themeDescriptions: Record<ThemeType, string> = {
    government: 'Professional design for government organizations with formal blue tones',
    enterprise: 'Modern corporate design with sleek dark theme for businesses',
    university: 'Vibrant and creative design with purple gradients for educational institutions',
    global: 'Fresh and dynamic design with cyan colors for international services',
  };

  const handleSelectTheme = (themeType: ThemeType) => {
    dispatch(setTheme(themeType));
    setTimeout(() => {
      onClose();
    }, 500);
  };

  return (
    <Modal isOpen={isOpen} onClose={onClose} title="Select Theme" size="xl">
      <div className="space-y-6">
        <p className="text-gray-600 text-center">
          Choose a theme that best represents your organization
        </p>

        <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
          {Object.entries(themePresets).map(([key, theme]) => {
            const themeType = key as ThemeType;
            const isSelected = currentTheme === themeType;

            return (
              <button
                key={key}
                onClick={() => handleSelectTheme(themeType)}
                className={`relative p-6 rounded-2xl border-2 transition-all duration-300 transform hover:scale-105 ${
                  isSelected
                    ? 'border-blue-500 bg-blue-50'
                    : 'border-gray-200 hover:border-gray-300 hover:shadow-lg'
                }`}
              >
                {/* Selected Badge */}
                {isSelected && (
                  <div className="absolute top-4 right-4 w-8 h-8 bg-blue-500 text-white rounded-full flex items-center justify-center">
                    <FaCheck />
                  </div>
                )}

                {/* Theme Preview */}
                <div
                  className="h-32 rounded-xl mb-4 flex items-center justify-center text-white"
                  style={{
                    background: theme.gradients.primary,
                    boxShadow: theme.effects.shadow,
                  }}
                >
                  {themeIcons[themeType]}
                </div>

                {/* Theme Info */}
                <div className="text-left">
                  <h3 className="text-xl font-bold text-gray-800 mb-2">
                    {theme.name}
                  </h3>
                  <p className="text-sm text-gray-600 leading-relaxed">
                    {themeDescriptions[themeType]}
                  </p>
                </div>

                {/* Color Palette */}
                <div className="mt-4 flex gap-2">
                  <div
                    className="w-8 h-8 rounded-full border-2 border-white shadow-md"
                    style={{ backgroundColor: theme.colors.primary }}
                  />
                  <div
                    className="w-8 h-8 rounded-full border-2 border-white shadow-md"
                    style={{ backgroundColor: theme.colors.secondary }}
                  />
                  <div
                    className="w-8 h-8 rounded-full border-2 border-white shadow-md"
                    style={{ backgroundColor: theme.colors.accent }}
                  />
                </div>
              </button>
            );
          })}
        </div>

        {/* Preview Info */}
        <div className="bg-gray-50 p-4 rounded-xl">
          <p className="text-sm text-gray-600 text-center">
            💡 The theme will be applied immediately across the entire application
          </p>
        </div>
      </div>
    </Modal>
  );
};

export default ThemeSelector;

 

/components/Sidebar/Sidebar.tsx

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { FaChevronDown, FaChevronRight } from 'react-icons/fa';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { addTab } from '../../features/ui/uiSlice';
import { menuConfig, getIconComponent } from '../../utils/menuConfig';
import type { MenuItem } from '../../types';

const Sidebar = () => {
  const navigate = useNavigate();
  const dispatch = useAppDispatch();
  const { sidebarCollapsed } = useAppSelector((state) => state.ui);
  const { config } = useAppSelector((state) => state.theme);
  const [expandedMenus, setExpandedMenus] = useState<string[]>(['basic']);

  const toggleMenu = (menuId: string) => {
    setExpandedMenus((prev) =>
      prev.includes(menuId)
        ? prev.filter((id) => id !== menuId)
        : [...prev, menuId]
    );
  };

  const handleMenuItemClick = (item: MenuItem, _parentTitle: string) => {
    if (item.path) {
      // Add tab to Redux store
      dispatch(
        addTab({
          id: item.id,
          title: item.title,
          path: item.path,
        })
      );
      
      // Navigate to the route
      navigate(item.path);
    }
  };

  const renderMenuItem = (item: MenuItem, level: number = 0, parentTitle?: string) => {
    const hasChildren = item.children && item.children.length > 0;
    const isExpanded = expandedMenus.includes(item.id);
    const IconComponent = item.icon ? getIconComponent(item.icon) : null;

    if (level === 0) {
      // Parent menu item
      return (
        <div key={item.id} className="mb-2">
          <button
            onClick={() => toggleMenu(item.id)}
            className={`w-full flex items-center justify-between px-4 py-3.5 rounded-xl transition-all duration-300 transform hover:scale-[1.02] ${
              sidebarCollapsed 
                ? 'justify-center' 
                : 'hover:bg-white/20 hover:shadow-lg'
            } ${isExpanded ? 'bg-white/15' : ''}`}
          >
            <div className="flex items-center gap-3">
              {IconComponent && (
                <div className="w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center backdrop-blur-sm">
                  <IconComponent className="text-lg flex-shrink-0" />
                </div>
              )}
              {!sidebarCollapsed && (
                <span className="font-semibold text-sm">{item.title}</span>
              )}
            </div>
            {!sidebarCollapsed && (
              <span className={`text-sm transition-transform duration-300 ${isExpanded ? 'rotate-180' : ''}`}>
                <FaChevronDown />
              </span>
            )}
          </button>

          {/* Children */}
          {hasChildren && isExpanded && !sidebarCollapsed && (
            <div className="mt-2 ml-4 space-y-1 pl-4 border-l-2 border-white/20">
              {item.children!.map((child) =>
                renderMenuItem(child, level + 1, item.title)
              )}
            </div>
          )}
        </div>
      );
    } else {
      // Child menu item
      return (
        <button
          key={item.id}
          onClick={() => handleMenuItemClick(item, parentTitle || '')}
          className="w-full text-left px-4 py-2.5 rounded-lg hover:bg-white/15 transition-all duration-300 text-sm flex items-center gap-2 group"
        >
          <span className="w-1.5 h-1.5 rounded-full bg-white/40 group-hover:bg-white group-hover:scale-150 transition-all duration-300" />
          {item.title}
        </button>
      );
    }
  };

  return (
    <aside
      className={`text-white h-full transition-all duration-300 shadow-2xl overflow-y-auto ${
        sidebarCollapsed ? 'w-20' : 'w-72'
      }`}
      style={{
        background: config.gradients.sidebar,
        boxShadow: config.effects.shadowHover,
      }}
    >
      {/* Sidebar Header */}
      {!sidebarCollapsed && (
        <div className="p-6 border-b border-white/20">
          <h2 className="text-xs font-semibold uppercase tracking-wider opacity-70">
            Navigation Menu
          </h2>
        </div>
      )}

      <nav className="p-4 space-y-2">
        {menuConfig.map((item) => renderMenuItem(item))}
      </nav>

      {/* Sidebar Footer */}
      {!sidebarCollapsed && (
        <div className="p-6 mt-auto border-t border-white/20">
          <div className="text-xs text-center opacity-60">
            <p>v1.0.0</p>
            <p className="mt-1">© 2024 WMS</p>
          </div>
        </div>
      )}
    </aside>
  );
};

export default Sidebar;

 

/components/Tabs/Tabs.tsx

import { useNavigate } from 'react-router-dom';
import { FaTimes } from 'react-icons/fa';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { removeTab, setActiveTab } from '../../features/ui/uiSlice';

const Tabs = () => {
  const navigate = useNavigate();
  const dispatch = useAppDispatch();
  const { tabs, activeTabId } = useAppSelector((state) => state.ui);
  const { config } = useAppSelector((state) => state.theme);

  const handleTabClick = (tabId: string, path: string) => {
    dispatch(setActiveTab(tabId));
    navigate(path);
  };

  const handleCloseTab = (e: React.MouseEvent, tabId: string) => {
    e.stopPropagation();
    dispatch(removeTab(tabId));
  };

  if (tabs.length === 0) {
    return null;
  }

  return (
    <div className="bg-gradient-to-r from-gray-50 to-gray-100 border-b border-gray-200 flex items-center overflow-x-auto shadow-sm">
      {tabs.map((tab, index) => {
        const isActive = activeTabId === tab.id;
        
        return (
          <div
            key={tab.id}
            onClick={() => handleTabClick(tab.id, tab.path)}
            className={`
              relative flex items-center gap-2 px-5 py-3 cursor-pointer
              transition-all duration-300 whitespace-nowrap
              ${
                isActive
                  ? 'bg-white text-gray-900 font-semibold shadow-md'
                  : 'text-gray-600 hover:bg-white/50 hover:text-gray-900'
              }
              ${index === 0 ? 'rounded-tl-lg' : ''}
            `}
            style={{
              borderTop: isActive ? `3px solid ${config.colors.primary}` : '3px solid transparent',
            }}
          >
            {/* Tab Number Badge */}
            <span
              className={`
                w-5 h-5 rounded-full flex items-center justify-center text-xs font-bold
                transition-all duration-300
                ${
                  isActive
                    ? 'text-white'
                    : 'bg-gray-200 text-gray-600'
                }
              `}
              style={{
                backgroundColor: isActive ? config.colors.primary : undefined,
              }}
            >
              {index + 1}
            </span>

            {/* Tab Title */}
            <span className="text-sm max-w-[150px] truncate">{tab.title}</span>

            {/* Close Button */}
            {tabs.length > 1 && (
              <button
                onClick={(e) => handleCloseTab(e, tab.id)}
                className={`
                  ml-1 p-1.5 rounded-full transition-all duration-300
                  ${
                    isActive
                      ? 'hover:bg-gray-100'
                      : 'hover:bg-gray-200'
                  }
                `}
                aria-label={`Close ${tab.title}`}
              >
                <FaTimes className="text-xs" />
              </button>
            )}

            {/* Active Indicator Line */}
            {isActive && (
              <div
                className="absolute bottom-0 left-0 right-0 h-0.5"
                style={{ backgroundColor: config.colors.primary }}
              />
            )}
          </div>
        );
      })}
    </div>
  );
};

export default Tabs;

/pages/Dashboard/DashboardPage.tsx

// import { useEffect } from 'react';
import { 
  FaBell, 
  FaExclamationCircle, 
  FaBoxes, 
  FaTruck, 
  FaChartLine,
  FaStar
} from 'react-icons/fa';
import { useAppDispatch, useAppSelector } from '../../hooks';
import { markAsRead } from '../../features/notifications/notificationsSlice';
import { announcementsAPI } from '../../api';

const DashboardPage = () => {
  const dispatch = useAppDispatch();
  const { announcements, unreadCount } = useAppSelector(
    (state) => state.notifications
  );
  const { user, affiliation } = useAppSelector((state) => state.auth);
  const { config } = useAppSelector((state) => state.theme);

  const handleMarkAsRead = async (id: string) => {
    try {
      await announcementsAPI.markAsRead(id);
      dispatch(markAsRead(id));
    } catch (error) {
      console.error('Failed to mark announcement as read:', error);
    }
  };

  const formatDate = (dateString: string) => {
    const date = new Date(dateString);
    return date.toLocaleDateString('en-US', {
      year: 'numeric',
      month: 'short',
      day: 'numeric',
    });
  };

  const stats = [
    {
      title: 'Total Items',
      value: '1,234',
      icon: <FaBoxes className="text-3xl" />,
      color: config.colors.primary,
      bgColor: `${config.colors.primary}15`,
      change: '+12%',
    },
    {
      title: 'Pending Orders',
      value: '56',
      icon: <FaExclamationCircle className="text-3xl" />,
      color: config.colors.warning,
      bgColor: '#f59e0b15',
      change: '+5%',
    },
    {
      title: 'Active Tasks',
      value: '23',
      icon: <FaTruck className="text-3xl" />,
      color: config.colors.success,
      bgColor: '#10b98115',
      change: '-3%',
    },
    {
      title: 'Unread Notices',
      value: String(unreadCount),
      icon: <FaBell className="text-3xl" />,
      color: config.colors.error,
      bgColor: '#ef444415',
      change: 'New',
    },
  ];

  return (
    <div className="p-6 space-y-6">
      {/* Welcome Section */}
      <div 
        className="rounded-2xl shadow-xl p-8 text-white relative overflow-hidden"
        style={{
          background: config.gradients.primary,
        }}
      >
        {/* Decorative Elements */}
        <div className="absolute top-0 right-0 w-64 h-64 bg-white/10 rounded-full -mr-32 -mt-32 blur-3xl" />
        <div className="absolute bottom-0 left-0 w-64 h-64 bg-white/10 rounded-full -ml-32 -mb-32 blur-3xl" />
        
        <div className="relative z-10">
          <div className="flex items-center gap-3 mb-3">
            <div className="w-16 h-16 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm">
              <FaStar className="text-3xl" />
            </div>
            <div>
              <h1 className="text-3xl font-bold">
                Welcome back, {user?.fullName || user?.username}!
              </h1>
              <p className="text-white/80 mt-1">
                {affiliation?.companyName} · {affiliation?.departmentName}
              </p>
            </div>
          </div>
          <p className="text-white/70 text-sm mt-4">
            {new Date().toLocaleDateString('en-US', {
              weekday: 'long',
              year: 'numeric',
              month: 'long',
              day: 'numeric',
            })}
          </p>
        </div>
      </div>

      {/* Quick Stats */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
        {stats.map((stat, index) => (
          <div
            key={index}
            className="bg-white rounded-2xl shadow-lg p-6 transform hover:scale-105 transition-all duration-300 cursor-pointer"
            style={{
              boxShadow: config.effects.shadow,
            }}
          >
            <div className="flex items-center justify-between mb-4">
              <div
                className="w-14 h-14 rounded-xl flex items-center justify-center"
                style={{
                  backgroundColor: stat.bgColor,
                  color: stat.color,
                }}
              >
                {stat.icon}
              </div>
              <span
                className={`text-sm font-semibold px-3 py-1 rounded-full ${
                  stat.change.includes('+')
                    ? 'bg-green-100 text-green-700'
                    : stat.change.includes('-')
                    ? 'bg-red-100 text-red-700'
                    : 'bg-blue-100 text-blue-700'
                }`}
              >
                {stat.change}
              </span>
            </div>
            <p className="text-sm text-gray-600 mb-1">{stat.title}</p>
            <p className="text-3xl font-bold text-gray-800">{stat.value}</p>
          </div>
        ))}
      </div>

      {/* Performance Chart */}
      <div className="bg-white rounded-2xl shadow-lg p-6">
        <div className="flex items-center justify-between mb-6">
          <h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
            <FaChartLine style={{ color: config.colors.primary }} />
            Performance Overview
          </h2>
          <select className="px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:border-transparent">
            <option>Last 7 days</option>
            <option>Last 30 days</option>
            <option>Last 3 months</option>
          </select>
        </div>
        <div className="h-64 flex items-center justify-center bg-gray-50 rounded-xl">
          <p className="text-gray-500">Chart placeholder - Connect to backend for real data</p>
        </div>
      </div>

      {/* Announcements */}
      <div className="bg-white rounded-2xl shadow-lg">
        <div className="px-6 py-5 border-b border-gray-200">
          <h2 className="text-xl font-bold text-gray-800 flex items-center gap-2">
            <FaBell style={{ color: config.colors.primary }} />
            Announcements & Notices
            {unreadCount > 0 && (
              <span 
                className="ml-2 px-2.5 py-0.5 rounded-full text-xs font-semibold text-white"
                style={{ backgroundColor: config.colors.error }}
              >
                {unreadCount} New
              </span>
            )}
          </h2>
        </div>

        <div className="divide-y divide-gray-200">
          {announcements.length === 0 ? (
            <div className="px-6 py-12 text-center text-gray-500">
              <FaBell className="text-4xl mx-auto mb-3 opacity-30" />
              <p>No announcements at this time</p>
            </div>
          ) : (
            announcements.map((announcement) => (
              <div
                key={announcement.id}
                className={`px-6 py-5 hover:bg-gray-50 transition-all duration-300 cursor-pointer ${
                  !announcement.isRead ? 'bg-blue-50/50 border-l-4' : ''
                }`}
                style={{
                  borderLeftColor: !announcement.isRead ? config.colors.primary : 'transparent',
                }}
                onClick={() => !announcement.isRead && handleMarkAsRead(announcement.id)}
              >
                <div className="flex items-start justify-between gap-4">
                  <div className="flex-1">
                    <div className="flex items-center gap-2 mb-2">
                      <h3
                        className={`font-semibold text-lg ${
                          announcement.isImportant
                            ? 'text-red-600'
                            : 'text-gray-800'
                        }`}
                      >
                        {announcement.title}
                      </h3>
                      {announcement.isImportant && (
                        <span className="bg-red-100 text-red-600 text-xs font-semibold px-2.5 py-1 rounded-full">
                          Important
                        </span>
                      )}
                      {!announcement.isRead && (
                        <span 
                          className="text-white text-xs font-semibold px-2.5 py-1 rounded-full"
                          style={{ backgroundColor: config.colors.primary }}
                        >
                          New
                        </span>
                      )}
                    </div>
                    <p className="text-gray-600 mb-3 leading-relaxed">
                      {announcement.content}
                    </p>
                    <div className="flex items-center gap-3 text-sm text-gray-400">
                      <span>{formatDate(announcement.date)}</span>
                    </div>
                  </div>
                </div>
              </div>
            ))
          )}
        </div>
      </div>
    </div>
  );
};

export default DashboardPage;

 

 

npm run build 2>&1 | head -50

 

 

 

반응형