feat: Update UI with ARGE ICT branding and add profile management
- Add ARGE ICT color palette to Tailwind config - Add company logo to login and sidebar - Remove default credentials from login page (security) - Add profile management page for admins - Add password change functionality - Update all UI components with new color scheme - Add backend endpoints for profile update and password change
This commit is contained in:
parent
9a2745cd92
commit
1b1d2651f3
|
|
@ -110,9 +110,92 @@ def logout(current_admin):
|
||||||
)
|
)
|
||||||
db.session.add(log)
|
db.session.add(log)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'status': 'success',
|
'status': 'success',
|
||||||
'message': 'Logged out successfully'
|
'message': 'Logged out successfully'
|
||||||
}), 200
|
}), 200
|
||||||
|
|
||||||
|
@auth_bp.route('/profile', methods=['PUT'])
|
||||||
|
@token_required
|
||||||
|
def update_profile(current_admin):
|
||||||
|
"""Update admin profile"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
|
||||||
|
# Update allowed fields
|
||||||
|
if 'full_name' in data:
|
||||||
|
current_admin.full_name = data['full_name']
|
||||||
|
|
||||||
|
if 'email' in data:
|
||||||
|
# Check if email is already taken by another admin
|
||||||
|
existing = AdminUser.query.filter_by(email=data['email']).first()
|
||||||
|
if existing and existing.id != current_admin.id:
|
||||||
|
return jsonify({'error': 'Email already in use'}), 400
|
||||||
|
current_admin.email = data['email']
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Log action
|
||||||
|
log = AuditLog(
|
||||||
|
admin_id=current_admin.id,
|
||||||
|
action='update_profile',
|
||||||
|
details=f"Updated profile information",
|
||||||
|
ip_address=request.remote_addr
|
||||||
|
)
|
||||||
|
db.session.add(log)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Profile updated successfully',
|
||||||
|
'admin': current_admin.to_dict()
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
@auth_bp.route('/change-password', methods=['POST'])
|
||||||
|
@token_required
|
||||||
|
def change_password(current_admin):
|
||||||
|
"""Change admin password"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
current_password = data.get('current_password')
|
||||||
|
new_password = data.get('new_password')
|
||||||
|
|
||||||
|
if not current_password or not new_password:
|
||||||
|
return jsonify({'error': 'Current and new password required'}), 400
|
||||||
|
|
||||||
|
# Verify current password
|
||||||
|
if not current_admin.check_password(current_password):
|
||||||
|
return jsonify({'error': 'Current password is incorrect'}), 401
|
||||||
|
|
||||||
|
# Validate new password
|
||||||
|
if len(new_password) < 8:
|
||||||
|
return jsonify({'error': 'New password must be at least 8 characters'}), 400
|
||||||
|
|
||||||
|
# Update password
|
||||||
|
current_admin.set_password(new_password)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
# Log action
|
||||||
|
log = AuditLog(
|
||||||
|
admin_id=current_admin.id,
|
||||||
|
action='change_password',
|
||||||
|
details='Password changed successfully',
|
||||||
|
ip_address=request.remote_addr
|
||||||
|
)
|
||||||
|
db.session.add(log)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'status': 'success',
|
||||||
|
'message': 'Password changed successfully'
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import Dashboard from './pages/Dashboard'
|
||||||
import Plans from './pages/Plans'
|
import Plans from './pages/Plans'
|
||||||
import CFAccounts from './pages/CFAccounts'
|
import CFAccounts from './pages/CFAccounts'
|
||||||
import Customers from './pages/Customers'
|
import Customers from './pages/Customers'
|
||||||
|
import Profile from './pages/Profile'
|
||||||
import PrivateRoute from './components/PrivateRoute'
|
import PrivateRoute from './components/PrivateRoute'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
@ -17,6 +18,7 @@ function App() {
|
||||||
<Route path="/plans" element={<PrivateRoute><Plans /></PrivateRoute>} />
|
<Route path="/plans" element={<PrivateRoute><Plans /></PrivateRoute>} />
|
||||||
<Route path="/cf-accounts" element={<PrivateRoute><CFAccounts /></PrivateRoute>} />
|
<Route path="/cf-accounts" element={<PrivateRoute><CFAccounts /></PrivateRoute>} />
|
||||||
<Route path="/customers" element={<PrivateRoute><Customers /></PrivateRoute>} />
|
<Route path="/customers" element={<PrivateRoute><Customers /></PrivateRoute>} />
|
||||||
|
<Route path="/profile" element={<PrivateRoute><Profile /></PrivateRoute>} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,18 @@ const Layout = ({ children }) => {
|
||||||
<div className="min-h-screen flex">
|
<div className="min-h-screen flex">
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
<aside className="w-64 bg-white border-r border-gray-200">
|
<aside className="w-64 bg-white border-r border-gray-200">
|
||||||
<div className="p-6">
|
<div className="p-6 border-b border-gray-200">
|
||||||
<h1 className="text-2xl font-bold text-primary-600">Admin Panel</h1>
|
<div className="flex items-center gap-3 mb-2">
|
||||||
<p className="text-sm text-gray-500 mt-1">Hosting Platform</p>
|
<img
|
||||||
|
src="/arge-logo.svg"
|
||||||
|
alt="ARGE ICT"
|
||||||
|
className="h-10 w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-500">Admin Panel</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="px-4 space-y-1">
|
<nav className="px-4 py-4 space-y-1">
|
||||||
{menuItems.map((item) => {
|
{menuItems.map((item) => {
|
||||||
const isActive = location.pathname === item.path;
|
const isActive = location.pathname === item.path;
|
||||||
return (
|
return (
|
||||||
|
|
@ -47,8 +53,11 @@ const Layout = ({ children }) => {
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div className="absolute bottom-0 w-64 p-4 border-t border-gray-200">
|
<div className="absolute bottom-0 w-64 p-4 border-t border-gray-200 bg-white">
|
||||||
<div className="flex items-center gap-3 mb-3">
|
<Link
|
||||||
|
to="/profile"
|
||||||
|
className="flex items-center gap-3 mb-3 p-2 rounded-lg hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
<div className="w-10 h-10 bg-primary-100 rounded-full flex items-center justify-center">
|
<div className="w-10 h-10 bg-primary-100 rounded-full flex items-center justify-center">
|
||||||
<span className="text-primary-600 font-semibold">
|
<span className="text-primary-600 font-semibold">
|
||||||
{admin?.username?.charAt(0).toUpperCase()}
|
{admin?.username?.charAt(0).toUpperCase()}
|
||||||
|
|
@ -60,10 +69,10 @@ const Layout = ({ children }) => {
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 truncate">{admin?.email}</p>
|
<p className="text-xs text-gray-500 truncate">{admin?.email}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="w-full btn-secondary text-sm"
|
className="w-full btn-outline text-sm"
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -71,7 +80,7 @@ const Layout = ({ children }) => {
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<main className="flex-1 overflow-auto">
|
<main className="flex-1 overflow-auto bg-gray-50">
|
||||||
<div className="p-8">{children}</div>
|
<div className="p-8">{children}</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -57,8 +57,13 @@ export const AuthProvider = ({ children }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateAdmin = (updatedAdmin) => {
|
||||||
|
localStorage.setItem('admin_user', JSON.stringify(updatedAdmin));
|
||||||
|
setAdmin(updatedAdmin);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ admin, login, logout, loading }}>
|
<AuthContext.Provider value={{ admin, login, logout, updateAdmin, loading }}>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -10,55 +10,67 @@
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@apply px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors font-medium;
|
@apply px-4 py-2 bg-primary-500 text-white rounded-lg hover:bg-primary-600 transition-all duration-200 font-medium shadow-sm hover:shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
@apply px-4 py-2 bg-white text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors font-medium;
|
@apply px-4 py-2 bg-secondary-500 text-white rounded-lg hover:bg-secondary-600 transition-all duration-200 font-medium shadow-sm hover:shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
@apply px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 transition-colors font-medium;
|
@apply px-4 py-2 bg-danger-500 text-white rounded-lg hover:bg-danger-600 transition-all duration-200 font-medium shadow-sm hover:shadow-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn-accent {
|
||||||
|
@apply px-4 py-2 bg-accent-500 text-white rounded-lg hover:bg-accent-600 transition-all duration-200 font-medium shadow-sm hover:shadow-md;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
@apply px-4 py-2 bg-white text-gray-700 border border-gray-300 rounded-lg hover:bg-gray-50 transition-all duration-200 font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
.input-field {
|
.input-field {
|
||||||
@apply w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent outline-none transition-all;
|
@apply w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500 outline-none transition-all duration-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
@apply bg-white rounded-lg shadow-sm border border-gray-200 p-6;
|
@apply bg-white rounded-lg shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow duration-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
@apply w-full border-collapse;
|
@apply w-full border-collapse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table th {
|
.table th {
|
||||||
@apply bg-gray-50 text-left text-xs font-medium text-gray-500 uppercase tracking-wider px-6 py-3 border-b border-gray-200;
|
@apply bg-primary-50 text-left text-xs font-medium text-primary-700 uppercase tracking-wider px-6 py-3 border-b border-primary-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table td {
|
.table td {
|
||||||
@apply px-6 py-4 whitespace-nowrap text-sm text-gray-900 border-b border-gray-200;
|
@apply px-6 py-4 whitespace-nowrap text-sm text-gray-900 border-b border-gray-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-success {
|
.badge-success {
|
||||||
@apply bg-green-100 text-green-800;
|
@apply bg-success-100 text-success-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-warning {
|
.badge-warning {
|
||||||
@apply bg-yellow-100 text-yellow-800;
|
@apply bg-accent-100 text-accent-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-danger {
|
.badge-danger {
|
||||||
@apply bg-red-100 text-red-800;
|
@apply bg-danger-100 text-danger-800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-info {
|
.badge-info {
|
||||||
@apply bg-blue-100 text-blue-800;
|
@apply bg-primary-100 text-primary-800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-secondary {
|
||||||
|
@apply bg-secondary-100 text-secondary-800;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,17 +33,25 @@ const Login = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 to-primary-100">
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-primary-50 via-secondary-50 to-primary-100">
|
||||||
<div className="w-full max-w-md">
|
<div className="w-full max-w-md">
|
||||||
<div className="card">
|
<div className="card shadow-2xl">
|
||||||
<div className="text-center mb-8">
|
<div className="text-center mb-8">
|
||||||
<h1 className="text-3xl font-bold text-gray-900 mb-2">Admin Panel</h1>
|
{/* ARGE ICT Logo */}
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<img
|
||||||
|
src="/arge-logo.svg"
|
||||||
|
alt="ARGE ICT"
|
||||||
|
className="h-16 w-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-3xl font-bold text-primary-500 mb-2">Admin Panel</h1>
|
||||||
<p className="text-gray-600">Hosting Platform Management</p>
|
<p className="text-gray-600">Hosting Platform Management</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
<div className="mb-4 p-4 bg-danger-50 border border-danger-200 rounded-lg">
|
||||||
<p className="text-red-800 text-sm">{error}</p>
|
<p className="text-danger-800 text-sm">{error}</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -85,14 +93,6 @@ const Login = () => {
|
||||||
{loading ? 'Logging in...' : 'Login'}
|
{loading ? 'Logging in...' : 'Login'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
||||||
<p className="text-xs text-blue-800">
|
|
||||||
<strong>Default credentials:</strong><br />
|
|
||||||
Username: admin<br />
|
|
||||||
Password: admin123
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useAuth } from '../context/AuthContext';
|
||||||
|
import api from '../services/api';
|
||||||
|
|
||||||
|
const Profile = () => {
|
||||||
|
const { admin, updateAdmin } = useAuth();
|
||||||
|
const [activeTab, setActiveTab] = useState('profile');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [message, setMessage] = useState({ type: '', text: '' });
|
||||||
|
|
||||||
|
// Profile form state
|
||||||
|
const [profileData, setProfileData] = useState({
|
||||||
|
full_name: admin?.full_name || '',
|
||||||
|
email: admin?.email || '',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Password form state
|
||||||
|
const [passwordData, setPasswordData] = useState({
|
||||||
|
current_password: '',
|
||||||
|
new_password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleProfileUpdate = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setMessage({ type: '', text: '' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await api.put('/auth/profile', profileData);
|
||||||
|
updateAdmin(response.data.admin);
|
||||||
|
setMessage({ type: 'success', text: 'Profile updated successfully!' });
|
||||||
|
} catch (error) {
|
||||||
|
setMessage({
|
||||||
|
type: 'error',
|
||||||
|
text: error.response?.data?.error || 'Failed to update profile'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePasswordChange = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
setMessage({ type: '', text: '' });
|
||||||
|
|
||||||
|
// Validate passwords match
|
||||||
|
if (passwordData.new_password !== passwordData.confirm_password) {
|
||||||
|
setMessage({ type: 'error', text: 'New passwords do not match' });
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate password length
|
||||||
|
if (passwordData.new_password.length < 8) {
|
||||||
|
setMessage({ type: 'error', text: 'Password must be at least 8 characters' });
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.post('/auth/change-password', {
|
||||||
|
current_password: passwordData.current_password,
|
||||||
|
new_password: passwordData.new_password,
|
||||||
|
});
|
||||||
|
setMessage({ type: 'success', text: 'Password changed successfully!' });
|
||||||
|
setPasswordData({
|
||||||
|
current_password: '',
|
||||||
|
new_password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
setMessage({
|
||||||
|
type: 'error',
|
||||||
|
text: error.response?.data?.error || 'Failed to change password'
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-4xl mx-auto">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-6">Account Settings</h1>
|
||||||
|
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="mb-6 border-b border-gray-200">
|
||||||
|
<nav className="-mb-px flex space-x-8">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('profile')}
|
||||||
|
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||||
|
activeTab === 'profile'
|
||||||
|
? 'border-primary-500 text-primary-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Profile Information
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('password')}
|
||||||
|
className={`py-4 px-1 border-b-2 font-medium text-sm ${
|
||||||
|
activeTab === 'password'
|
||||||
|
? 'border-primary-500 text-primary-600'
|
||||||
|
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Change Password
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message Alert */}
|
||||||
|
{message.text && (
|
||||||
|
<div className={`mb-6 p-4 rounded-lg ${
|
||||||
|
message.type === 'success'
|
||||||
|
? 'bg-success-50 border border-success-200 text-success-800'
|
||||||
|
: 'bg-danger-50 border border-danger-200 text-danger-800'
|
||||||
|
}`}>
|
||||||
|
{message.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Profile Tab */}
|
||||||
|
{activeTab === 'profile' && (
|
||||||
|
<div className="card">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 mb-6">Profile Information</h2>
|
||||||
|
<form onSubmit={handleProfileUpdate} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={admin?.username || ''}
|
||||||
|
disabled
|
||||||
|
className="input-field bg-gray-50 cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Username cannot be changed</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Full Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={profileData.full_name}
|
||||||
|
onChange={(e) => setProfileData({ ...profileData, full_name: e.target.value })}
|
||||||
|
className="input-field"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Email
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
value={profileData.email}
|
||||||
|
onChange={(e) => setProfileData({ ...profileData, email: e.target.value })}
|
||||||
|
className="input-field"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Role
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={admin?.role || ''}
|
||||||
|
disabled
|
||||||
|
className="input-field bg-gray-50 cursor-not-allowed"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="btn-primary disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Updating...' : 'Update Profile'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Password Tab */}
|
||||||
|
{activeTab === 'password' && (
|
||||||
|
<div className="card">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 mb-6">Change Password</h2>
|
||||||
|
<form onSubmit={handlePasswordChange} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Current Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={passwordData.current_password}
|
||||||
|
onChange={(e) => setPasswordData({ ...passwordData, current_password: e.target.value })}
|
||||||
|
className="input-field"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
New Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={passwordData.new_password}
|
||||||
|
onChange={(e) => setPasswordData({ ...passwordData, new_password: e.target.value })}
|
||||||
|
className="input-field"
|
||||||
|
required
|
||||||
|
minLength={8}
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-sm text-gray-500">Minimum 8 characters</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
Confirm New Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={passwordData.confirm_password}
|
||||||
|
onChange={(e) => setPasswordData({ ...passwordData, confirm_password: e.target.value })}
|
||||||
|
className="input-field"
|
||||||
|
required
|
||||||
|
minLength={8}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="btn-primary disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{loading ? 'Changing...' : 'Change Password'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Profile;
|
||||||
|
|
||||||
|
|
@ -7,17 +7,66 @@ export default {
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
// ARGE ICT Brand Colors
|
||||||
primary: {
|
primary: {
|
||||||
50: '#eff6ff',
|
50: '#e6f2ff',
|
||||||
100: '#dbeafe',
|
100: '#cce5ff',
|
||||||
200: '#bfdbfe',
|
200: '#99cbff',
|
||||||
300: '#93c5fd',
|
300: '#66b0ff',
|
||||||
400: '#60a5fa',
|
400: '#3396ff',
|
||||||
500: '#3b82f6',
|
500: '#0F578B', // Main blue
|
||||||
600: '#2563eb',
|
600: '#0c4670',
|
||||||
700: '#1d4ed8',
|
700: '#093554',
|
||||||
800: '#1e40af',
|
800: '#062438',
|
||||||
900: '#1e3a8a',
|
900: '#03121c',
|
||||||
|
},
|
||||||
|
secondary: {
|
||||||
|
50: '#e6f7f0',
|
||||||
|
100: '#ccefe0',
|
||||||
|
200: '#99dfc1',
|
||||||
|
300: '#66cfa2',
|
||||||
|
400: '#33bf83',
|
||||||
|
500: '#159052', // Main green
|
||||||
|
600: '#117342',
|
||||||
|
700: '#0d5631',
|
||||||
|
800: '#083921',
|
||||||
|
900: '#041d10',
|
||||||
|
},
|
||||||
|
accent: {
|
||||||
|
50: '#fef3e6',
|
||||||
|
100: '#fde7cc',
|
||||||
|
200: '#fbcf99',
|
||||||
|
300: '#f9b766',
|
||||||
|
400: '#f79f33',
|
||||||
|
500: '#F69036', // Orange
|
||||||
|
600: '#c5732b',
|
||||||
|
700: '#945620',
|
||||||
|
800: '#623a16',
|
||||||
|
900: '#311d0b',
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
50: '#fce8ea',
|
||||||
|
100: '#f9d1d4',
|
||||||
|
200: '#f3a3a9',
|
||||||
|
300: '#ed757e',
|
||||||
|
400: '#e74753',
|
||||||
|
500: '#B42832', // Main red
|
||||||
|
600: '#902028',
|
||||||
|
700: '#6c181e',
|
||||||
|
800: '#481014',
|
||||||
|
900: '#24080a',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
50: '#e9f7f0',
|
||||||
|
100: '#d3efe1',
|
||||||
|
200: '#a7dfc3',
|
||||||
|
300: '#7bcfa5',
|
||||||
|
400: '#4fbf87',
|
||||||
|
500: '#046D3F', // Dark green
|
||||||
|
600: '#035732',
|
||||||
|
700: '#024126',
|
||||||
|
800: '#022c19',
|
||||||
|
900: '#01160d',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue