alpe-temp 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/frontend-project/src/App.css +1 -0
- package/frontend-project/src/Auth/Login.jsx +85 -0
- package/frontend-project/src/Auth/Register.jsx +183 -0
- package/frontend-project/src/Intro.jsx +33 -0
- package/frontend-project/src/LayOut.jsx +38 -0
- package/frontend-project/src/api/ApiClient.js +92 -0
- package/frontend-project/src/assets/hero.png +0 -0
- package/frontend-project/src/assets/react.svg +1 -0
- package/frontend-project/src/assets/vite.svg +1 -0
- package/frontend-project/src/components/Aside.jsx +9 -0
- package/frontend-project/src/components/Button.jsx +100 -0
- package/frontend-project/src/components/Card.jsx +104 -0
- package/frontend-project/src/components/FormField.jsx +129 -0
- package/frontend-project/src/components/Modal.jsx +106 -0
- package/frontend-project/src/components/Table.jsx +127 -0
- package/frontend-project/src/components/Toast.jsx +64 -0
- package/frontend-project/src/components/index.js +14 -0
- package/frontend-project/src/config.js +66 -0
- package/frontend-project/src/design.js +115 -0
- package/frontend-project/src/index.css +60 -0
- package/frontend-project/src/layouts/BottomNav.jsx +156 -0
- package/frontend-project/src/layouts/TopNav.jsx +150 -0
- package/frontend-project/src/layouts/useShell.js +44 -0
- package/frontend-project/src/main.jsx +41 -0
- package/frontend-project/src/pages/Department.jsx +188 -0
- package/frontend-project/src/pages/Employee.jsx +274 -0
- package/frontend-project/src/pages/Home.jsx +79 -0
- package/frontend-project/src/pages/Profile.jsx +9 -0
- package/frontend-project/src/pages/Register.jsx +57 -0
- package/frontend-project/src/pages/Reports.jsx +91 -0
- package/frontend-project/src/pages/Salary.jsx +264 -0
- package/frontend-project/src/themes.js +175 -0
- package/package.json +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// 📐 DESIGN TOKENS — Turns config.js into CSS variables and utilities
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS FILE DOES:
|
|
6
|
+
// Takes your config.js settings and turns them into:
|
|
7
|
+
// 1. CSS custom properties (variables) for global styling
|
|
8
|
+
// 2. Tailwind-compatible class names for components
|
|
9
|
+
//
|
|
10
|
+
// HOW IT WORKS:
|
|
11
|
+
// The Layout component calls `getDesignTokens(config)` to get an object
|
|
12
|
+
// with CSS variable values and utility class names. These are applied
|
|
13
|
+
// to the root div so every child component can use them.
|
|
14
|
+
//
|
|
15
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
|
|
17
|
+
import { getTheme } from './themes';
|
|
18
|
+
|
|
19
|
+
// ─── RADIUS VALUES ───────────────────────────────────────────────────────────
|
|
20
|
+
const RADIUS = {
|
|
21
|
+
none: '0px',
|
|
22
|
+
sm: '0.125rem', // 2px
|
|
23
|
+
md: '0.375rem', // 6px
|
|
24
|
+
lg: '0.5rem', // 8px
|
|
25
|
+
xl: '0.75rem', // 12px
|
|
26
|
+
full: '9999px',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// ─── FONT SIZE SCALES ────────────────────────────────────────────────────────
|
|
30
|
+
const FONT_SCALE = {
|
|
31
|
+
normal: {
|
|
32
|
+
xs: '0.75rem', // 12px
|
|
33
|
+
sm: '0.8125rem', // 13px
|
|
34
|
+
base:'0.875rem', // 14px
|
|
35
|
+
lg: '1rem', // 16px
|
|
36
|
+
xl: '1.25rem', // 20px
|
|
37
|
+
'2xl':'1.5rem', // 24px
|
|
38
|
+
'3xl':'2rem', // 32px
|
|
39
|
+
},
|
|
40
|
+
large: {
|
|
41
|
+
xs: '0.8125rem', // 13px
|
|
42
|
+
sm: '0.875rem', // 14px
|
|
43
|
+
base:'1rem', // 16px
|
|
44
|
+
lg: '1.125rem', // 18px
|
|
45
|
+
xl: '1.375rem', // 22px
|
|
46
|
+
'2xl':'1.75rem', // 28px
|
|
47
|
+
'3xl':'2.25rem', // 36px
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// ─── ROUNDED CLASS HELPER ────────────────────────────────────────────────────
|
|
52
|
+
// Use this in components to get the Tailwind rounded class for a given level.
|
|
53
|
+
// Example: getRoundedClass('md') → 'rounded-md'
|
|
54
|
+
//
|
|
55
|
+
export function getRoundedClass(level = 'md') {
|
|
56
|
+
const map = {
|
|
57
|
+
none: 'rounded-none',
|
|
58
|
+
sm: 'rounded-sm',
|
|
59
|
+
md: 'rounded-md',
|
|
60
|
+
lg: 'rounded-lg',
|
|
61
|
+
xl: 'rounded-xl',
|
|
62
|
+
full: 'rounded-full',
|
|
63
|
+
};
|
|
64
|
+
return map[level] || map.md;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── GENERATE DESIGN TOKENS ──────────────────────────────────────────────────
|
|
68
|
+
// This is called once by the Layout component to produce:
|
|
69
|
+
// {
|
|
70
|
+
// cssVars: { '--color-primary': '#008A75', ... },
|
|
71
|
+
// roundedClass: 'rounded-md',
|
|
72
|
+
// theme: { ... resolved theme colors },
|
|
73
|
+
// }
|
|
74
|
+
//
|
|
75
|
+
export function getDesignTokens(config) {
|
|
76
|
+
const theme = getTheme(config.theme, config.colors);
|
|
77
|
+
|
|
78
|
+
// Merge user color overrides
|
|
79
|
+
const colors = { ...theme };
|
|
80
|
+
if (config.colors) {
|
|
81
|
+
Object.entries(config.colors).forEach(([key, val]) => {
|
|
82
|
+
if (val) colors[key] = val;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Build CSS variable map
|
|
87
|
+
const cssVars = {
|
|
88
|
+
'--color-primary': colors.primary,
|
|
89
|
+
'--color-surface': colors.surface,
|
|
90
|
+
'--color-card': colors.card,
|
|
91
|
+
'--color-border': colors.border,
|
|
92
|
+
'--color-text': colors.text,
|
|
93
|
+
'--color-text-muted': colors.textMuted,
|
|
94
|
+
'--color-nav-bg': colors.navBg,
|
|
95
|
+
'--color-nav-text': colors.navText,
|
|
96
|
+
'--color-nav-active': colors.navActive,
|
|
97
|
+
'--color-danger': colors.danger,
|
|
98
|
+
'--color-success': colors.success,
|
|
99
|
+
'--color-warning': colors.warning,
|
|
100
|
+
'--color-info': colors.info,
|
|
101
|
+
'--radius': RADIUS[config.rounded] || RADIUS.md,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Add font size variables
|
|
105
|
+
const fontSizes = FONT_SCALE[config.fontSize] || FONT_SCALE.normal;
|
|
106
|
+
Object.entries(fontSizes).forEach(([key, val]) => {
|
|
107
|
+
cssVars[`--text-${key}`] = val;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
cssVars,
|
|
112
|
+
roundedClass: getRoundedClass(config.rounded),
|
|
113
|
+
theme: colors,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
🎨 GLOBAL STYLES — Tailwind + CSS custom properties
|
|
3
|
+
═══════════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
CSS VARIABLES (--color-*, --radius, --text-*):
|
|
6
|
+
These are set dynamically by design.js based on your config.js.
|
|
7
|
+
They are applied to the :root element by the Layout component.
|
|
8
|
+
|
|
9
|
+
HOW TO USE IN COMPONENTS:
|
|
10
|
+
style={{ color: 'var(--color-primary)' }}
|
|
11
|
+
or Tailwind: className="bg-[var(--color-primary)]"
|
|
12
|
+
|
|
13
|
+
═══════════════════════════════════════════════════════════════════════════ */
|
|
14
|
+
|
|
15
|
+
@tailwind base;
|
|
16
|
+
@tailwind components;
|
|
17
|
+
@tailwind utilities;
|
|
18
|
+
|
|
19
|
+
/* ─── Custom CSS Variables (applied by Layout component) ───────────────── */
|
|
20
|
+
:root {
|
|
21
|
+
--color-primary: #008A75;
|
|
22
|
+
--color-surface: #F9FAFB;
|
|
23
|
+
--color-card: #FFFFFF;
|
|
24
|
+
--color-border: #E5E7EB;
|
|
25
|
+
--color-text: #1F2937;
|
|
26
|
+
--color-text-muted: #9CA3AF;
|
|
27
|
+
--color-nav-bg: #FFFFFF;
|
|
28
|
+
--color-nav-text: #4B5563;
|
|
29
|
+
--color-nav-active: #008A75;
|
|
30
|
+
--color-danger: #EF4444;
|
|
31
|
+
--color-success: #10B981;
|
|
32
|
+
--color-warning: #F59E0B;
|
|
33
|
+
--color-info: #3B82F6;
|
|
34
|
+
--radius: 0.375rem;
|
|
35
|
+
--text-xs: 0.75rem;
|
|
36
|
+
--text-sm: 0.8125rem;
|
|
37
|
+
--text-base: 0.875rem;
|
|
38
|
+
--text-lg: 1rem;
|
|
39
|
+
--text-xl: 1.25rem;
|
|
40
|
+
--text-2xl: 1.5rem;
|
|
41
|
+
--text-3xl: 2rem;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* ─── Tailwind-like utilities using CSS variables ──────────────────────── */
|
|
45
|
+
@layer utilities {
|
|
46
|
+
.bg-primary { background-color: var(--color-primary); }
|
|
47
|
+
.text-primary { color: var(--color-primary); }
|
|
48
|
+
.border-primary { border-color: var(--color-primary); }
|
|
49
|
+
.bg-surface { background-color: var(--color-surface); }
|
|
50
|
+
.bg-card { background-color: var(--color-card); }
|
|
51
|
+
.text-muted { color: var(--color-text-muted); }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* ─── Animations ───────────────────────────────────────────────────────── */
|
|
55
|
+
@keyframes spin {
|
|
56
|
+
to { transform: rotate(360deg); }
|
|
57
|
+
}
|
|
58
|
+
.animate-spin {
|
|
59
|
+
animation: spin 1s linear infinite;
|
|
60
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// 📱 BOTTOM NAV LAYOUT — Navbar at the bottom (mobile-first style)
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS FILE DOES:
|
|
6
|
+
// Renders the app shell with a bottom navigation bar.
|
|
7
|
+
// Switch to this by setting navigation: 'bottomnav' in config.js
|
|
8
|
+
//
|
|
9
|
+
// HOW IT LOOKS:
|
|
10
|
+
// ┌────────────────────────────────┐
|
|
11
|
+
// │ │
|
|
12
|
+
// │ PAGE CONTENT │ ← scrollable
|
|
13
|
+
// │ │
|
|
14
|
+
// ├────────────────────────────────┤
|
|
15
|
+
// │ 📊 👥 💰 🏢 📄 │ ← bottom nav (fixed)
|
|
16
|
+
// └────────────────────────────────┘
|
|
17
|
+
//
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
|
|
20
|
+
import React, { useState } from 'react';
|
|
21
|
+
import { NavLink, Outlet } from 'react-router-dom';
|
|
22
|
+
import {
|
|
23
|
+
LayoutDashboard, Users, DollarSign, Building2,
|
|
24
|
+
FileText, User, LogOut, ChevronDown,
|
|
25
|
+
} from 'lucide-react';
|
|
26
|
+
import { useShell } from './useShell';
|
|
27
|
+
|
|
28
|
+
// ─── NAVIGATION ITEMS ────────────────────────────────────────────────────────
|
|
29
|
+
// Add new pages here. The icon + label + path = one nav item.
|
|
30
|
+
//
|
|
31
|
+
const NAV_ITEMS = [
|
|
32
|
+
{ label: 'Home', path: '/dashboard/overview', Icon: LayoutDashboard },
|
|
33
|
+
{ label: 'Employees',path: '/dashboard/employees', Icon: Users },
|
|
34
|
+
{ label: 'Salary', path: '/dashboard/salary', Icon: DollarSign },
|
|
35
|
+
{ label: 'Depts', path: '/dashboard/departments', Icon: Building2 },
|
|
36
|
+
{ label: 'Reports', path: '/dashboard/reports', Icon: FileText },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export default function BottomNav() {
|
|
40
|
+
const { user, handleLogout } = useShell();
|
|
41
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex flex-col h-screen overflow-hidden">
|
|
45
|
+
{/* ─── TOP BAR (user menu, logo) ─────────────────────────────────── */}
|
|
46
|
+
<header
|
|
47
|
+
className="flex-shrink-0 flex items-center h-[52px] px-4 gap-1 z-20"
|
|
48
|
+
style={{
|
|
49
|
+
backgroundColor: 'var(--color-nav-bg)',
|
|
50
|
+
color: 'var(--color-nav-text)',
|
|
51
|
+
borderBottom: '1px solid var(--color-border)',
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<div className="flex items-center gap-2 mr-4">
|
|
55
|
+
<img src="/logo.png" alt="logo" className="w-7 h-9" />
|
|
56
|
+
<div className="flex flex-col mt-4">
|
|
57
|
+
<span className="text-[15px] font-bold tracking-tight leading-tight">
|
|
58
|
+
EP<span style={{ color: 'var(--color-primary)' }}>MS</span>
|
|
59
|
+
</span>
|
|
60
|
+
<span className="text-[7px] font-medium uppercase tracking-wider leading-tight opacity-60">
|
|
61
|
+
Employee Payroll
|
|
62
|
+
</span>
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
|
|
66
|
+
{/* Spacer */}
|
|
67
|
+
<div className="flex-1" />
|
|
68
|
+
|
|
69
|
+
{/* User dropdown */}
|
|
70
|
+
<div className="relative">
|
|
71
|
+
<button
|
|
72
|
+
onClick={() => setMenuOpen((v) => !v)}
|
|
73
|
+
className="flex items-center gap-2 px-3 py-1.5 text-[12px] font-medium transition-colors hover:opacity-80"
|
|
74
|
+
>
|
|
75
|
+
<div
|
|
76
|
+
className="w-6 h-6 flex items-center justify-center"
|
|
77
|
+
style={{ backgroundColor: 'var(--color-primary)', color: '#fff' }}
|
|
78
|
+
>
|
|
79
|
+
<User className="w-3 h-3" />
|
|
80
|
+
</div>
|
|
81
|
+
<span className="hidden sm:inline">{user?.name ?? 'Admin'}</span>
|
|
82
|
+
<ChevronDown className={`w-3 h-3 transition-transform ${menuOpen ? 'rotate-180' : ''}`} />
|
|
83
|
+
</button>
|
|
84
|
+
{menuOpen && (
|
|
85
|
+
<>
|
|
86
|
+
<div className="fixed inset-0 z-10" onClick={() => setMenuOpen(false)} />
|
|
87
|
+
<div
|
|
88
|
+
className="absolute right-0 top-full mt-1 z-20 shadow-lg py-1 min-w-[150px]"
|
|
89
|
+
style={{ backgroundColor: 'var(--color-card)', border: '1px solid var(--color-border)' }}
|
|
90
|
+
>
|
|
91
|
+
<div className="px-3 py-2 border-b" style={{ borderColor: 'var(--color-border)' }}>
|
|
92
|
+
<p className="text-[12px] font-semibold" style={{ color: 'var(--color-text)' }}>
|
|
93
|
+
{user?.name ?? 'Admin'}
|
|
94
|
+
</p>
|
|
95
|
+
<p className="text-[11px]" style={{ color: 'var(--color-text-muted)' }}>
|
|
96
|
+
{user?.email ?? ''}
|
|
97
|
+
</p>
|
|
98
|
+
</div>
|
|
99
|
+
<button
|
|
100
|
+
onClick={() => { setMenuOpen(false); handleLogout(); }}
|
|
101
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-[12px] font-medium transition-colors"
|
|
102
|
+
style={{ color: 'var(--color-danger)' }}
|
|
103
|
+
onMouseEnter={(e) => e.target.style.backgroundColor = 'var(--color-danger)'}
|
|
104
|
+
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
|
|
105
|
+
>
|
|
106
|
+
<LogOut className="w-3.5 h-3.5" />
|
|
107
|
+
Logout
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
</header>
|
|
114
|
+
|
|
115
|
+
{/* ─── MAIN CONTENT ──────────────────────────────────────────────── */}
|
|
116
|
+
<main
|
|
117
|
+
className="flex-1 overflow-y-auto"
|
|
118
|
+
style={{
|
|
119
|
+
backgroundColor: 'var(--color-surface)',
|
|
120
|
+
padding: 'var(--text-base)',
|
|
121
|
+
}}
|
|
122
|
+
>
|
|
123
|
+
<div className="max-w-6xl mx-auto">
|
|
124
|
+
<Outlet />
|
|
125
|
+
</div>
|
|
126
|
+
</main>
|
|
127
|
+
|
|
128
|
+
{/* ─── BOTTOM NAV ──────────────────────────────────────────────────── */}
|
|
129
|
+
<nav
|
|
130
|
+
className="flex-shrink-0 flex items-center justify-around h-[64px] px-2 z-20"
|
|
131
|
+
style={{
|
|
132
|
+
backgroundColor: 'var(--color-nav-bg)',
|
|
133
|
+
borderTop: '1px solid var(--color-border)',
|
|
134
|
+
}}
|
|
135
|
+
>
|
|
136
|
+
{NAV_ITEMS.map(({ label, path, Icon }) => (
|
|
137
|
+
<NavLink
|
|
138
|
+
key={path}
|
|
139
|
+
to={path}
|
|
140
|
+
className="flex flex-col items-center gap-0.5 px-2 py-1 text-[10px] font-medium transition-colors"
|
|
141
|
+
style={({ isActive }) => ({
|
|
142
|
+
color: isActive ? 'var(--color-nav-active)' : 'var(--color-nav-text)',
|
|
143
|
+
})}
|
|
144
|
+
>
|
|
145
|
+
{({ isActive }) => (
|
|
146
|
+
<>
|
|
147
|
+
<Icon className="w-5 h-5" />
|
|
148
|
+
<span>{label}</span>
|
|
149
|
+
</>
|
|
150
|
+
)}
|
|
151
|
+
</NavLink>
|
|
152
|
+
))}
|
|
153
|
+
</nav>
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
+
// 📐 TOP NAV LAYOUT — Navbar at the top (classic web style)
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// WHAT THIS FILE DOES:
|
|
6
|
+
// Renders the app shell with a horizontal nav bar at the top.
|
|
7
|
+
// This is the default layout (navigation: 'topnav' in config.js).
|
|
8
|
+
//
|
|
9
|
+
// HOW IT LOOKS:
|
|
10
|
+
// ┌──────────────────────────────────────────────┐
|
|
11
|
+
// │ LOGO Home Employees Salary Reports 👤│ ← top nav (fixed)
|
|
12
|
+
// ├──────────────────────────────────────────────┤
|
|
13
|
+
// │ │
|
|
14
|
+
// │ PAGE CONTENT │ ← scrollable
|
|
15
|
+
// │ │
|
|
16
|
+
// └──────────────────────────────────────────────┘
|
|
17
|
+
//
|
|
18
|
+
// HOW TO ADD A NEW NAV ITEM:
|
|
19
|
+
// Just add an entry to the NAV_ITEMS array below.
|
|
20
|
+
// Example: { label: 'Products', path: '/dashboard/products', Icon: Package }
|
|
21
|
+
//
|
|
22
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
+
|
|
24
|
+
import React, { useState } from 'react';
|
|
25
|
+
import { NavLink, Outlet } from 'react-router-dom';
|
|
26
|
+
import {
|
|
27
|
+
LayoutDashboard, Users, DollarSign, Building2,
|
|
28
|
+
FileText, User, LogOut, ChevronDown,
|
|
29
|
+
} from 'lucide-react';
|
|
30
|
+
import { useShell } from './useShell';
|
|
31
|
+
|
|
32
|
+
// ─── NAVIGATION ITEMS ────────────────────────────────────────────────────────
|
|
33
|
+
// To add a new page, just add an object to this array.
|
|
34
|
+
// Each item needs: label (shown text), path (URL), Icon (from lucide-react)
|
|
35
|
+
//
|
|
36
|
+
const NAV_ITEMS = [
|
|
37
|
+
{ label: 'Overview', path: '/dashboard/overview', Icon: LayoutDashboard },
|
|
38
|
+
{ label: 'Employees', path: '/dashboard/employees', Icon: Users },
|
|
39
|
+
{ label: 'Salary', path: '/dashboard/salary', Icon: DollarSign },
|
|
40
|
+
{ label: 'Departments', path: '/dashboard/departments', Icon: Building2 },
|
|
41
|
+
{ label: 'Reports', path: '/dashboard/reports', Icon: FileText },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
export default function TopNav() {
|
|
45
|
+
const { user, handleLogout } = useShell();
|
|
46
|
+
const [menuOpen, setMenuOpen] = useState(false);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex flex-col h-screen overflow-hidden">
|
|
50
|
+
{/* ─── TOP NAV BAR ───────────────────────────────────────────────── */}
|
|
51
|
+
<header
|
|
52
|
+
className="flex-shrink-0 flex items-center h-[52px] px-4 gap-1 z-20"
|
|
53
|
+
style={{
|
|
54
|
+
backgroundColor: 'var(--color-nav-bg)',
|
|
55
|
+
color: 'var(--color-nav-text)',
|
|
56
|
+
borderBottom: '1px solid var(--color-border)',
|
|
57
|
+
}}
|
|
58
|
+
>
|
|
59
|
+
{/* Logo + Brand */}
|
|
60
|
+
<div className="flex items-center gap-2 mr-4 pr-4" style={{ borderRight: '1px solid var(--color-border)' }}>
|
|
61
|
+
<img src="/logo.png" alt="logo" className="w-7 h-9" />
|
|
62
|
+
<div className="flex flex-col mt-4">
|
|
63
|
+
<span className="text-[15px] font-bold tracking-tight leading-tight">
|
|
64
|
+
EP<span style={{ color: 'var(--color-primary)' }}>MS</span>
|
|
65
|
+
</span>
|
|
66
|
+
<span className="text-[7px] font-medium uppercase tracking-wider leading-tight opacity-60">
|
|
67
|
+
Employee Payroll
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* Navigation links */}
|
|
73
|
+
<nav className="flex items-center gap-0.5 flex-1">
|
|
74
|
+
{NAV_ITEMS.map(({ label, path, Icon }) => (
|
|
75
|
+
<NavLink
|
|
76
|
+
key={path}
|
|
77
|
+
to={path}
|
|
78
|
+
className="flex items-center gap-1.5 px-3 py-1.5 text-[12px] font-medium transition-colors"
|
|
79
|
+
style={({ isActive }) => ({
|
|
80
|
+
backgroundColor: isActive ? 'var(--color-primary)' : 'transparent',
|
|
81
|
+
color: isActive ? '#ffffff' : 'var(--color-nav-text)',
|
|
82
|
+
})}
|
|
83
|
+
>
|
|
84
|
+
<Icon className="w-3.5 h-3.5 flex-shrink-0" />
|
|
85
|
+
<span className="hidden sm:inline">{label}</span>
|
|
86
|
+
</NavLink>
|
|
87
|
+
))}
|
|
88
|
+
</nav>
|
|
89
|
+
|
|
90
|
+
{/* User dropdown */}
|
|
91
|
+
<div className="relative ml-auto">
|
|
92
|
+
<button
|
|
93
|
+
onClick={() => setMenuOpen((v) => !v)}
|
|
94
|
+
className="flex items-center gap-2 px-3 py-1.5 text-[12px] font-medium transition-colors hover:opacity-80"
|
|
95
|
+
>
|
|
96
|
+
<div
|
|
97
|
+
className="w-6 h-6 flex items-center justify-center"
|
|
98
|
+
style={{ backgroundColor: 'var(--color-primary)', color: '#fff' }}
|
|
99
|
+
>
|
|
100
|
+
<User className="w-3 h-3" />
|
|
101
|
+
</div>
|
|
102
|
+
<span className="hidden sm:inline">{user?.name ?? 'Admin'}</span>
|
|
103
|
+
<ChevronDown className={`w-3 h-3 transition-transform ${menuOpen ? 'rotate-180' : ''}`} />
|
|
104
|
+
</button>
|
|
105
|
+
{menuOpen && (
|
|
106
|
+
<>
|
|
107
|
+
<div className="fixed inset-0 z-10" onClick={() => setMenuOpen(false)} />
|
|
108
|
+
<div
|
|
109
|
+
className="absolute right-0 top-full mt-1 z-20 shadow-lg py-1 min-w-[150px]"
|
|
110
|
+
style={{ backgroundColor: 'var(--color-card)', border: '1px solid var(--color-border)' }}
|
|
111
|
+
>
|
|
112
|
+
<div className="px-3 py-2 border-b" style={{ borderColor: 'var(--color-border)' }}>
|
|
113
|
+
<p className="text-[12px] font-semibold" style={{ color: 'var(--color-text)' }}>
|
|
114
|
+
{user?.name ?? 'Admin'}
|
|
115
|
+
</p>
|
|
116
|
+
<p className="text-[11px]" style={{ color: 'var(--color-text-muted)' }}>
|
|
117
|
+
{user?.email ?? ''}
|
|
118
|
+
</p>
|
|
119
|
+
</div>
|
|
120
|
+
<button
|
|
121
|
+
onClick={() => { setMenuOpen(false); handleLogout(); }}
|
|
122
|
+
className="w-full flex items-center gap-2 px-3 py-2 text-[12px] font-medium transition-colors"
|
|
123
|
+
style={{ color: 'var(--color-danger)' }}
|
|
124
|
+
onMouseEnter={(e) => e.target.style.backgroundColor = 'var(--color-danger)'}
|
|
125
|
+
onMouseLeave={(e) => e.target.style.backgroundColor = 'transparent'}
|
|
126
|
+
>
|
|
127
|
+
<LogOut className="w-3.5 h-3.5" />
|
|
128
|
+
Logout
|
|
129
|
+
</button>
|
|
130
|
+
</div>
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</header>
|
|
135
|
+
|
|
136
|
+
{/* ─── MAIN CONTENT ──────────────────────────────────────────────── */}
|
|
137
|
+
<main
|
|
138
|
+
className="flex-1 overflow-y-auto"
|
|
139
|
+
style={{
|
|
140
|
+
backgroundColor: 'var(--color-surface)',
|
|
141
|
+
padding: '24px',
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
<div className="max-w-7xl mx-auto">
|
|
145
|
+
<Outlet />
|
|
146
|
+
</div>
|
|
147
|
+
</main>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* layouts/useShell.js – Shared auth logic for every layout shell.
|
|
3
|
+
*
|
|
4
|
+
* Returns { user, handleLogout } — plug into any shell component.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useEffect, useState } from 'react';
|
|
8
|
+
import { useNavigate } from 'react-router-dom';
|
|
9
|
+
import { authApi } from '../api/ApiClient';
|
|
10
|
+
|
|
11
|
+
export function useShell() {
|
|
12
|
+
const [user, setUser] = useState(null);
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const token = localStorage.getItem('token');
|
|
17
|
+
if (!token) { navigate('/'); return; }
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const payload = JSON.parse(atob(token.split('.')[1]));
|
|
21
|
+
if (payload.exp < Date.now() / 1000) {
|
|
22
|
+
localStorage.removeItem('token');
|
|
23
|
+
localStorage.removeItem('user');
|
|
24
|
+
navigate('/');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
navigate('/');
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stored = localStorage.getItem('user');
|
|
33
|
+
setUser(stored ? JSON.parse(stored) : {});
|
|
34
|
+
}, [navigate]);
|
|
35
|
+
|
|
36
|
+
const handleLogout = async () => {
|
|
37
|
+
try { await authApi.logout(); } catch { /* ignore */ }
|
|
38
|
+
localStorage.removeItem('token');
|
|
39
|
+
localStorage.removeItem('user');
|
|
40
|
+
navigate('/');
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return { user, handleLogout };
|
|
44
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import './index.css'
|
|
4
|
+
import { createBrowserRouter , createRoutesFromElements , RouterProvider , Route } from 'react-router-dom'
|
|
5
|
+
import Intro from './Intro'
|
|
6
|
+
import Login from './Auth/Login'
|
|
7
|
+
import Register from './Auth/Register'
|
|
8
|
+
import LayOut from './LayOut'
|
|
9
|
+
import Home from './pages/Home'
|
|
10
|
+
import Employee from './pages/Employee'
|
|
11
|
+
import Salary from './pages/Salary'
|
|
12
|
+
import Department from './pages/Department'
|
|
13
|
+
import Reports from './pages/Reports'
|
|
14
|
+
import Profile from './pages/Profile'
|
|
15
|
+
import { ToastProvider } from './components'
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
const router = createBrowserRouter(
|
|
19
|
+
createRoutesFromElements(
|
|
20
|
+
<>
|
|
21
|
+
<Route path='/' element={<Intro />} />
|
|
22
|
+
<Route path='/login' element={<Login />} />
|
|
23
|
+
<Route path='/register' element={<Register />} />
|
|
24
|
+
<Route path='/dashboard' element={<LayOut />} >
|
|
25
|
+
<Route path='overview' element={<Home />} />
|
|
26
|
+
<Route path='employees' element={<Employee />} />
|
|
27
|
+
<Route path='salary' element={<Salary />} />
|
|
28
|
+
<Route path='departments' element={<Department />} />
|
|
29
|
+
<Route path='reports' element={<Reports />} />
|
|
30
|
+
<Route path='profile' element={<Profile />} />
|
|
31
|
+
</Route>
|
|
32
|
+
</>
|
|
33
|
+
)
|
|
34
|
+
)
|
|
35
|
+
createRoot(document.getElementById('root')).render(
|
|
36
|
+
<StrictMode>
|
|
37
|
+
<ToastProvider >
|
|
38
|
+
<RouterProvider router={router} />
|
|
39
|
+
</ToastProvider>
|
|
40
|
+
</StrictMode>,
|
|
41
|
+
)
|