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": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"axios": "^1.6.2",
|
"axios": "^1.6.2",
|
||||||
"react-router-dom": "^6.20.1"
|
"react-router-dom": "^6.20.1",
|
||||||
|
"@heroicons/react": "^2.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.43",
|
"@types/react": "^18.2.43",
|
||||||
|
|
|
||||||
|
|
@ -1,93 +1,45 @@
|
||||||
import { useState } from 'react'
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||||
import DomainSetup from './pages/DomainSetup'
|
import { AuthProvider, useAuth } from './context/AuthContext'
|
||||||
import DomainSetupNew from './pages/DomainSetupNew'
|
import Landing from './pages/Landing'
|
||||||
import DomainList from './pages/DomainList'
|
import Dashboard from './pages/Dashboard'
|
||||||
import AdminCFAccounts from './pages/AdminCFAccounts'
|
|
||||||
import './App.css'
|
import './App.css'
|
||||||
|
|
||||||
|
// Protected route wrapper
|
||||||
|
const ProtectedRoute = ({ children }) => {
|
||||||
|
const { isAuthenticated, loading } = useAuth()
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return isAuthenticated ? children : <Navigate to="/" replace />
|
||||||
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [currentPage, setCurrentPage] = useState('setup-new')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
|
<BrowserRouter>
|
||||||
{/* Header */}
|
<AuthProvider>
|
||||||
<header className="bg-white shadow-sm">
|
<Routes>
|
||||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
<Route path="/" element={<Landing />} />
|
||||||
<div className="flex items-center justify-between">
|
<Route
|
||||||
<div className="flex items-center space-x-3">
|
path="/dashboard"
|
||||||
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-lg flex items-center justify-center">
|
element={
|
||||||
<span className="text-white text-xl font-bold">H</span>
|
<ProtectedRoute>
|
||||||
</div>
|
<Dashboard />
|
||||||
<div>
|
</ProtectedRoute>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Hosting Platform</h1>
|
}
|
||||||
<p className="text-sm text-gray-500">DNS & SSL Management</p>
|
/>
|
||||||
</div>
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
</div>
|
</Routes>
|
||||||
|
</AuthProvider>
|
||||||
<nav className="flex space-x-4">
|
</BrowserRouter>
|
||||||
<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>
|
|
||||||
|
|
||||||
{/* 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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
@layer base {
|
||||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
* {
|
||||||
line-height: 1.5;
|
@apply border-gray-200;
|
||||||
font-weight: 400;
|
}
|
||||||
|
|
||||||
color-scheme: light dark;
|
body {
|
||||||
color: rgba(255, 255, 255, 0.87);
|
@apply bg-gray-50 text-gray-900 font-sans;
|
||||||
background-color: #242424;
|
}
|
||||||
|
|
||||||
font-synthesis: none;
|
|
||||||
text-rendering: optimizeLegibility;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
@layer components {
|
||||||
margin: 0;
|
/* Custom component styles */
|
||||||
display: flex;
|
.btn-primary {
|
||||||
place-items: center;
|
@apply bg-primary-500 hover:bg-primary-600 text-white font-medium px-4 py-2 rounded-lg transition-colors;
|
||||||
min-width: 320px;
|
}
|
||||||
min-height: 100vh;
|
|
||||||
}
|
.btn-secondary {
|
||||||
|
@apply bg-secondary-500 hover:bg-secondary-600 text-white font-medium px-4 py-2 rounded-lg transition-colors;
|
||||||
#root {
|
}
|
||||||
width: 100%;
|
|
||||||
margin: 0 auto;
|
.input-field {
|
||||||
text-align: center;
|
@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 = {
|
export const dnsAPI = {
|
||||||
// Health check
|
// Health check
|
||||||
health: () => api.get('/health'),
|
health: () => api.get('/health'),
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,49 @@ export default {
|
||||||
"./src/**/*.{js,ts,jsx,tsx}",
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
],
|
],
|
||||||
theme: {
|
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: [],
|
plugins: [],
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue