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
|
|
@ -116,3 +116,86 @@ def logout(current_admin):
|
|||
'message': 'Logged out successfully'
|
||||
}), 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 CFAccounts from './pages/CFAccounts'
|
||||
import Customers from './pages/Customers'
|
||||
import Profile from './pages/Profile'
|
||||
import PrivateRoute from './components/PrivateRoute'
|
||||
|
||||
function App() {
|
||||
|
|
@ -17,6 +18,7 @@ function App() {
|
|||
<Route path="/plans" element={<PrivateRoute><Plans /></PrivateRoute>} />
|
||||
<Route path="/cf-accounts" element={<PrivateRoute><CFAccounts /></PrivateRoute>} />
|
||||
<Route path="/customers" element={<PrivateRoute><Customers /></PrivateRoute>} />
|
||||
<Route path="/profile" element={<PrivateRoute><Profile /></PrivateRoute>} />
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
|
|
|
|||
|
|
@ -22,12 +22,18 @@ const Layout = ({ children }) => {
|
|||
<div className="min-h-screen flex">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-white border-r border-gray-200">
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold text-primary-600">Admin Panel</h1>
|
||||
<p className="text-sm text-gray-500 mt-1">Hosting Platform</p>
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<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>
|
||||
|
||||
<nav className="px-4 space-y-1">
|
||||
<nav className="px-4 py-4 space-y-1">
|
||||
{menuItems.map((item) => {
|
||||
const isActive = location.pathname === item.path;
|
||||
return (
|
||||
|
|
@ -47,8 +53,11 @@ const Layout = ({ children }) => {
|
|||
})}
|
||||
</nav>
|
||||
|
||||
<div className="absolute bottom-0 w-64 p-4 border-t border-gray-200">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div className="absolute bottom-0 w-64 p-4 border-t border-gray-200 bg-white">
|
||||
<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">
|
||||
<span className="text-primary-600 font-semibold">
|
||||
{admin?.username?.charAt(0).toUpperCase()}
|
||||
|
|
@ -60,10 +69,10 @@ const Layout = ({ children }) => {
|
|||
</p>
|
||||
<p className="text-xs text-gray-500 truncate">{admin?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full btn-secondary text-sm"
|
||||
className="w-full btn-outline text-sm"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
|
|
@ -71,7 +80,7 @@ const Layout = ({ children }) => {
|
|||
</aside>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
<main className="flex-1 overflow-auto bg-gray-50">
|
||||
<div className="p-8">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -57,8 +57,13 @@ export const AuthProvider = ({ children }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const updateAdmin = (updatedAdmin) => {
|
||||
localStorage.setItem('admin_user', JSON.stringify(updatedAdmin));
|
||||
setAdmin(updatedAdmin);
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ admin, login, logout, loading }}>
|
||||
<AuthContext.Provider value={{ admin, login, logout, updateAdmin, loading }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -10,23 +10,31 @@
|
|||
|
||||
@layer components {
|
||||
.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 {
|
||||
@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 {
|
||||
@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 {
|
||||
@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 {
|
||||
@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 {
|
||||
|
|
@ -34,7 +42,7 @@
|
|||
}
|
||||
|
||||
.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 {
|
||||
|
|
@ -46,19 +54,23 @@
|
|||
}
|
||||
|
||||
.badge-success {
|
||||
@apply bg-green-100 text-green-800;
|
||||
@apply bg-success-100 text-success-800;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
@apply bg-yellow-100 text-yellow-800;
|
||||
@apply bg-accent-100 text-accent-800;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
@apply bg-red-100 text-red-800;
|
||||
@apply bg-danger-100 text-danger-800;
|
||||
}
|
||||
|
||||
.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 (
|
||||
<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="card">
|
||||
<div className="card shadow-2xl">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<p className="text-red-800 text-sm">{error}</p>
|
||||
<div className="mb-4 p-4 bg-danger-50 border border-danger-200 rounded-lg">
|
||||
<p className="text-danger-800 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -85,14 +93,6 @@ const Login = () => {
|
|||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
extend: {
|
||||
colors: {
|
||||
// ARGE ICT Brand Colors
|
||||
primary: {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
50: '#e6f2ff',
|
||||
100: '#cce5ff',
|
||||
200: '#99cbff',
|
||||
300: '#66b0ff',
|
||||
400: '#3396ff',
|
||||
500: '#0F578B', // Main blue
|
||||
600: '#0c4670',
|
||||
700: '#093554',
|
||||
800: '#062438',
|
||||
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