Add frontend authentication - Landing page, Dashboard, Auth context
This commit is contained in:
parent
4676200874
commit
ec0164691b
|
|
@ -13,7 +13,8 @@
|
|||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"axios": "^1.6.2",
|
||||
"react-router-dom": "^6.20.1"
|
||||
"react-router-dom": "^6.20.1",
|
||||
"@heroicons/react": "^2.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
|
|
|
|||
|
|
@ -1,93 +1,45 @@
|
|||
import { useState } from 'react'
|
||||
import DomainSetup from './pages/DomainSetup'
|
||||
import DomainSetupNew from './pages/DomainSetupNew'
|
||||
import DomainList from './pages/DomainList'
|
||||
import AdminCFAccounts from './pages/AdminCFAccounts'
|
||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { AuthProvider, useAuth } from './context/AuthContext'
|
||||
import Landing from './pages/Landing'
|
||||
import Dashboard from './pages/Dashboard'
|
||||
import './App.css'
|
||||
|
||||
function App() {
|
||||
const [currentPage, setCurrentPage] = useState('setup-new')
|
||||
// Protected route wrapper
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { isAuthenticated, loading } = useAuth()
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
||||
{/* Header */}
|
||||
<header className="bg-white shadow-sm">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center">
|
||||
<span className="text-white text-xl font-bold">H</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">Hosting Platform</h1>
|
||||
<p className="text-sm text-gray-500">DNS & SSL Management</p>
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<div className="w-16 h-16 border-4 border-primary-500 border-t-transparent rounded-full animate-spin mx-auto"></div>
|
||||
<p className="mt-4 text-gray-600">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
<nav className="flex space-x-4">
|
||||
<button
|
||||
onClick={() => setCurrentPage('setup-new')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
currentPage === 'setup-new'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
🆕 Add Domain (New)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('setup')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
currentPage === 'setup'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
Add Domain (Old)
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('list')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
currentPage === 'list'
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
My Domains
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setCurrentPage('admin')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
currentPage === 'admin'
|
||||
? 'bg-purple-500 text-white'
|
||||
: 'text-gray-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
⚙️ Admin
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
return isAuthenticated ? children : <Navigate to="/" replace />
|
||||
}
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{currentPage === 'setup-new' && <DomainSetupNew />}
|
||||
{currentPage === 'setup' && <DomainSetup />}
|
||||
{currentPage === 'list' && <DomainList />}
|
||||
{currentPage === 'admin' && <AdminCFAccounts />}
|
||||
</main>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="bg-white border-t border-gray-200 mt-12">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<div className="text-center text-gray-500 text-sm">
|
||||
<p>© 2024 Hosting Platform. Powered by Cloudflare.</p>
|
||||
<p className="mt-1">Automated DNS & SSL Management</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<Routes>
|
||||
<Route path="/" element={<Landing />} />
|
||||
<Route
|
||||
path="/dashboard"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<Dashboard />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</AuthProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* Auth Context - Global authentication state management
|
||||
*/
|
||||
import { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { authAPI } from '../services/api';
|
||||
|
||||
const AuthContext = createContext(null);
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [customer, setCustomer] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
|
||||
// Check if user is logged in on mount
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
if (token) {
|
||||
try {
|
||||
const response = await authAPI.getProfile();
|
||||
setUser(response.data.user);
|
||||
setCustomer(response.data.customer);
|
||||
setIsAuthenticated(true);
|
||||
} catch (error) {
|
||||
console.error('Auth check failed:', error);
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user');
|
||||
}
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
const login = async (email, password) => {
|
||||
try {
|
||||
const response = await authAPI.login({ email, password });
|
||||
const { token, user: userData, customer: customerData } = response.data;
|
||||
|
||||
localStorage.setItem('auth_token', token);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
|
||||
setUser(userData);
|
||||
setCustomer(customerData);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
console.error('Login failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.message || 'Login failed',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const register = async (data) => {
|
||||
try {
|
||||
const response = await authAPI.register(data);
|
||||
const { token, user: userData, customer: customerData } = response.data;
|
||||
|
||||
localStorage.setItem('auth_token', token);
|
||||
localStorage.setItem('user', JSON.stringify(userData));
|
||||
|
||||
setUser(userData);
|
||||
setCustomer(customerData);
|
||||
setIsAuthenticated(true);
|
||||
|
||||
return { success: true, data: response.data };
|
||||
} catch (error) {
|
||||
console.error('Registration failed:', error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.response?.data?.message || 'Registration failed',
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
localStorage.removeItem('auth_token');
|
||||
localStorage.removeItem('user');
|
||||
setUser(null);
|
||||
setCustomer(null);
|
||||
setIsAuthenticated(false);
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
customer,
|
||||
loading,
|
||||
isAuthenticated,
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
};
|
||||
|
||||
|
|
@ -1,33 +1,35 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-gray-200;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
@apply bg-gray-50 text-gray-900 font-sans;
|
||||
}
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
@layer components {
|
||||
/* Custom component styles */
|
||||
.btn-primary {
|
||||
@apply bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-2 rounded-lg transition-colors;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
@apply bg-secondary-500 hover:bg-secondary-600 text-white font-medium px-4 py-2 rounded-lg transition-colors;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.card {
|
||||
@apply bg-white rounded-xl shadow-sm border border-gray-200 p-6;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,231 @@
|
|||
/**
|
||||
* Customer Dashboard - Main dashboard with sidebar navigation
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import {
|
||||
HomeIcon,
|
||||
GlobeAltIcon,
|
||||
ServerIcon,
|
||||
WifiIcon,
|
||||
ShieldCheckIcon,
|
||||
Cog6ToothIcon,
|
||||
ArrowRightOnRectangleIcon,
|
||||
} from '@heroicons/react/24/outline';
|
||||
|
||||
const Dashboard = () => {
|
||||
const { user, customer, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/');
|
||||
};
|
||||
|
||||
const menuItems = [
|
||||
{ id: 'overview', name: 'Overview', icon: HomeIcon },
|
||||
{ id: 'dns', name: 'DNS Management', icon: GlobeAltIcon },
|
||||
{ id: 'containers', name: 'Containers', icon: ServerIcon },
|
||||
{ id: 'network', name: 'Network', icon: WifiIcon },
|
||||
{ id: 'security', name: 'Security', icon: ShieldCheckIcon },
|
||||
{ id: 'settings', name: 'Settings', icon: Cog6ToothIcon },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 flex">
|
||||
{/* Sidebar */}
|
||||
<aside className="w-64 bg-white border-r border-gray-200 flex flex-col">
|
||||
{/* Logo */}
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<img
|
||||
src="https://www.argeict.com/wp-content/uploads/2016/09/arge_logo-4.svg"
|
||||
alt="ARGE ICT"
|
||||
className="h-10"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* User info */}
|
||||
<div className="p-6 border-b border-gray-200">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-primary-500 rounded-full flex items-center justify-center text-white font-semibold">
|
||||
{user?.full_name?.charAt(0) || 'U'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-gray-900 truncate">
|
||||
{user?.full_name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 truncate">{customer?.company_name || user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between text-xs">
|
||||
<span className="text-gray-500">Plan:</span>
|
||||
<span className="px-2 py-1 bg-primary-100 text-primary-700 rounded-full font-medium">
|
||||
{customer?.subscription_plan || 'Free'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-4 space-y-1">
|
||||
{menuItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
const isActive = activeTab === item.id;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setActiveTab(item.id)}
|
||||
className={`w-full flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${
|
||||
isActive
|
||||
? 'bg-primary-50 text-primary-700'
|
||||
: 'text-gray-700 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-5 h-5" />
|
||||
<span className="font-medium">{item.name}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="p-4 border-t border-gray-200">
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center space-x-3 px-4 py-3 text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
||||
>
|
||||
<ArrowRightOnRectangleIcon className="w-5 h-5" />
|
||||
<span className="font-medium">Logout</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 overflow-auto">
|
||||
<div className="p-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">
|
||||
{menuItems.find((item) => item.id === activeTab)?.name}
|
||||
</h1>
|
||||
<p className="text-gray-600 mt-1">
|
||||
Welcome back, {user?.full_name}!
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Content based on active tab */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{/* Stats cards */}
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Domains</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-1">0</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
of {customer?.max_domains} limit
|
||||
</p>
|
||||
</div>
|
||||
<GlobeAltIcon className="w-12 h-12 text-primary-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Containers</p>
|
||||
<p className="text-3xl font-bold text-gray-900 mt-1">0</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
of {customer?.max_containers} limit
|
||||
</p>
|
||||
</div>
|
||||
<ServerIcon className="w-12 h-12 text-secondary-500" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-600">Status</p>
|
||||
<p className="text-3xl font-bold text-green-600 mt-1">Active</p>
|
||||
<p className="text-xs text-gray-500 mt-1">All systems operational</p>
|
||||
</div>
|
||||
<ShieldCheckIcon className="w-12 h-12 text-green-500" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'dns' && (
|
||||
<div className="card">
|
||||
<p className="text-gray-600">DNS Management module coming soon...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'containers' && (
|
||||
<div className="card">
|
||||
<p className="text-gray-600">Container management module coming soon...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'network' && (
|
||||
<div className="card">
|
||||
<p className="text-gray-600">Network configuration module coming soon...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'security' && (
|
||||
<div className="card">
|
||||
<p className="text-gray-600">Security settings module coming soon...</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'settings' && (
|
||||
<div className="card">
|
||||
<h2 className="text-xl font-semibold mb-4">Account Settings</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={user?.email || ''}
|
||||
disabled
|
||||
className="input-field bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Full Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={user?.full_name || ''}
|
||||
disabled
|
||||
className="input-field bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Company Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customer?.company_name || ''}
|
||||
disabled
|
||||
className="input-field bg-gray-50"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
|
|
@ -0,0 +1,237 @@
|
|||
/**
|
||||
* Landing Page - Register/Login with animations
|
||||
*/
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const Landing = () => {
|
||||
const [isLogin, setIsLogin] = useState(true);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const navigate = useNavigate();
|
||||
const { login, register } = useAuth();
|
||||
|
||||
// Form states
|
||||
const [formData, setFormData] = useState({
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirm: '',
|
||||
full_name: '',
|
||||
company_name: '',
|
||||
});
|
||||
|
||||
const handleChange = (e) => {
|
||||
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
let result;
|
||||
if (isLogin) {
|
||||
result = await login(formData.email, formData.password);
|
||||
} else {
|
||||
result = await register(formData);
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
navigate('/dashboard');
|
||||
} else {
|
||||
setError(result.error);
|
||||
}
|
||||
} catch (err) {
|
||||
setError('An unexpected error occurred');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary-50 via-white to-secondary-50 flex items-center justify-center p-4">
|
||||
{/* Background decoration */}
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div className="absolute -top-40 -right-40 w-80 h-80 bg-primary-200 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob"></div>
|
||||
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-secondary-200 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-2000"></div>
|
||||
<div className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-80 h-80 bg-brand-green-light/20 rounded-full mix-blend-multiply filter blur-xl opacity-70 animate-blob animation-delay-4000"></div>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-md relative z-10">
|
||||
{/* Logo */}
|
||||
<div className="text-center mb-8">
|
||||
<img
|
||||
src="https://www.argeict.com/wp-content/uploads/2016/09/arge_logo-4.svg"
|
||||
alt="ARGE ICT"
|
||||
className="h-16 mx-auto mb-4"
|
||||
/>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Hosting Platform</h1>
|
||||
<p className="text-gray-600 mt-2">
|
||||
Professional WordPress hosting with container infrastructure
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Form Card */}
|
||||
<div className="card">
|
||||
{/* Tabs */}
|
||||
<div className="flex border-b border-gray-200 mb-6">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsLogin(true);
|
||||
setError('');
|
||||
}}
|
||||
className={`flex-1 py-3 text-center font-medium transition-colors ${
|
||||
isLogin
|
||||
? 'text-primary-600 border-b-2 border-primary-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Login
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsLogin(false);
|
||||
setError('');
|
||||
}}
|
||||
className={`flex-1 py-3 text-center font-medium transition-colors ${
|
||||
!isLogin
|
||||
? 'text-primary-600 border-b-2 border-primary-600'
|
||||
: 'text-gray-500 hover:text-gray-700'
|
||||
}`}
|
||||
>
|
||||
Register
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<div className="mb-4 p-3 bg-red-50 border border-red-200 text-red-700 rounded-lg text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Form */}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
{!isLogin && (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Full Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="full_name"
|
||||
value={formData.full_name}
|
||||
onChange={handleChange}
|
||||
className="input-field"
|
||||
required={!isLogin}
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Company Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="company_name"
|
||||
value={formData.company_name}
|
||||
onChange={handleChange}
|
||||
className="input-field"
|
||||
placeholder="Acme Inc (optional)"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email *
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
className="input-field"
|
||||
required
|
||||
placeholder="you@example.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Password *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password"
|
||||
value={formData.password}
|
||||
onChange={handleChange}
|
||||
className="input-field"
|
||||
required
|
||||
placeholder="••••••••"
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isLogin && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Confirm Password *
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
name="password_confirm"
|
||||
value={formData.password_confirm}
|
||||
onChange={handleChange}
|
||||
className="input-field"
|
||||
required={!isLogin}
|
||||
placeholder="••••••••"
|
||||
minLength={8}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full btn-primary disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loading ? 'Please wait...' : isLogin ? 'Login' : 'Create Account'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<p className="text-center text-sm text-gray-600 mt-6">
|
||||
© 2026 ARGE ICT. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>{`
|
||||
@keyframes blob {
|
||||
0%, 100% { transform: translate(0, 0) scale(1); }
|
||||
33% { transform: translate(30px, -50px) scale(1.1); }
|
||||
66% { transform: translate(-20px, 20px) scale(0.9); }
|
||||
}
|
||||
.animate-blob {
|
||||
animation: blob 7s infinite;
|
||||
}
|
||||
.animation-delay-2000 {
|
||||
animation-delay: 2s;
|
||||
}
|
||||
.animation-delay-4000 {
|
||||
animation-delay: 4s;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Landing;
|
||||
|
||||
|
|
@ -9,6 +9,42 @@ const api = axios.create({
|
|||
},
|
||||
})
|
||||
|
||||
// Request interceptor - Add auth token
|
||||
api.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = localStorage.getItem('auth_token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// Response interceptor - Handle errors
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Token expired or invalid
|
||||
localStorage.removeItem('auth_token')
|
||||
localStorage.removeItem('user')
|
||||
window.location.href = '/'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// Auth API
|
||||
export const authAPI = {
|
||||
register: (data) => api.post('/api/auth/register', data),
|
||||
login: (data) => api.post('/api/auth/login', data),
|
||||
getProfile: () => api.get('/api/auth/me'),
|
||||
verifyToken: (token) => api.post('/api/auth/verify-token', { token }),
|
||||
}
|
||||
|
||||
export const dnsAPI = {
|
||||
// Health check
|
||||
health: () => api.get('/health'),
|
||||
|
|
|
|||
|
|
@ -5,7 +5,49 @@ export default {
|
|||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
extend: {
|
||||
colors: {
|
||||
// Brand colors from ARGE ICT logo
|
||||
brand: {
|
||||
green: {
|
||||
DEFAULT: '#159052',
|
||||
dark: '#046D3F',
|
||||
light: '#53BA6F',
|
||||
},
|
||||
orange: '#F69036',
|
||||
blue: '#0F578B',
|
||||
red: '#B42832',
|
||||
},
|
||||
// Semantic colors
|
||||
primary: {
|
||||
50: '#e6f7ef',
|
||||
100: '#b3e6d0',
|
||||
200: '#80d5b1',
|
||||
300: '#4dc492',
|
||||
400: '#1ab373',
|
||||
500: '#159052', // Main brand green
|
||||
600: '#117342',
|
||||
700: '#0d5631',
|
||||
800: '#093921',
|
||||
900: '#051c10',
|
||||
},
|
||||
secondary: {
|
||||
50: '#fff3e6',
|
||||
100: '#ffdbb3',
|
||||
200: '#ffc380',
|
||||
300: '#ffab4d',
|
||||
400: '#ff931a',
|
||||
500: '#F69036', // Brand orange
|
||||
600: '#c5722b',
|
||||
700: '#945520',
|
||||
800: '#633816',
|
||||
900: '#321c0b',
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue