Skip to content

Instantly share code, notes, and snippets.

@xerudro
Created March 29, 2025 13:37
Show Gist options
  • Save xerudro/89a98638113041a5b4062311fbac2dcd to your computer and use it in GitHub Desktop.
Save xerudro/89a98638113041a5b4062311fbac2dcd to your computer and use it in GitHub Desktop.
Profile.tsx
// src/pages/Dashboard.tsx
import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
// --- Corrected Import Path ---
import { useAuth } from '../components/AuthContext';
// --- Layout and Page Components ---
import DashboardLayout from '../components/dashboard/DashboardLayout';
import DashboardOverview from '../components/dashboard/DashboardOverview';
import HostingServices from '../components/dashboard/services/HostingServices';
import DatabaseServices from '../components/dashboard/services/DatabaseServices';
import DomainServices from '../components/dashboard/services/DomainServices';
import EmailServices from '../components/dashboard/services/EmailServices';
import SSLServices from '../components/dashboard/services/SSLServices';
// --- REMOVED: Incorrect component for user dashboard ---
// import CurrencyServices from '../components/dashboard/services/CurrencyServices';
// --- Optionally import the user-facing converter ---
// import CurrencyConverter from '../components/CurrencyConverter';
// Import other dashboard pages as needed (Billing, Support, Settings)
const Dashboard = () => {
// Get user and loading state
const { user, isLoadingAuth } = useAuth();
// **RECOMMENDATION:** Move this logic to a <ProtectedRoute> component in App.tsx
// ProtectedRoute should handle isLoadingAuth and redirect to '/login' if !user
if (!isLoadingAuth && !user) {
console.log("Dashboard: User not authenticated, redirecting...");
// Redirect to login page is usually preferred for protected areas
return <Navigate to="/login" replace />;
}
// Optional: Show a loading indicator while initial auth check is running
// This prevents rendering the layout before knowing the auth status.
// If using ProtectedRoute, this check might not be needed here.
if (isLoadingAuth) {
return (
<div className="flex justify-center items-center min-h-screen bg-black">
{/* Add a suitable loading spinner */}
<div className="text-white">Loading Dashboard...</div>
</div>
);
}
// End of recommended changes related to auth check
return (
<DashboardLayout>
<Routes>
{/* Default dashboard route */}
<Route path="/" element={<DashboardOverview />} />
{/* Service-specific routes */}
<Route path="hosting" element={<HostingServices />} />
<Route path="databases" element={<DatabaseServices />} />
<Route path="domains" element={<DomainServices />} />
<Route path="email" element={<EmailServices />} />
<Route path="ssl" element={<SSLServices />} />
{/* --- REMOVED: Incorrect route for user dashboard --- */}
{/* <Route path="currency" element={<CurrencyServices />} /> */}
{/* --- Optional: Add route for user currency converter --- */}
{/* <Route path="currency-converter" element={<CurrencyConverter />} /> */}
{/* TODO: Add routes for Billing, Support, Settings */}
{/* <Route path="billing" element={<BillingPage />} /> */}
{/* <Route path="support" element={<SupportPage />} /> */}
{/* <Route path="settings" element={<SettingsPage />} /> */}
{/* Optional: Catch-all for unknown dashboard routes */}
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</DashboardLayout>
);
};
export default Dashboard;
// src/pages/Home.tsx
import React from 'react';
import Hero from '../components/Hero';
import Card from '../components/Card';
import CardGrid from '../components/CardGrid';
import { Server, Globe, Code, Shield, Cpu, Cloud, Database, Lock } from 'lucide-react';
const Home = () => {
// Feature data (unchanged)
const features = [
{ icon: <Server className="h-8 w-8 text-orangered" />, title: "High-Performance Hosting", description: "Lightning-fast servers with 99.9% uptime guarantee." },
{ icon: <Globe className="h-8 w-8 text-orangered" />, title: "Global CDN", description: "Content delivery network ensuring fast access worldwide." },
{ icon: <Code className="h-8 w-8 text-orangered" />, title: "Custom Development", description: "Tailored solutions built with cutting-edge technologies." },
{ icon: <Shield className="h-8 w-8 text-orangered" />, title: "Advanced Security", description: "Enterprise-grade security measures to protect your assets." },
{ icon: <Cpu className="h-8 w-8 text-orangered" />, title: "Scalable Infrastructure", description: "Grow your applications with our flexible infrastructure." },
{ icon: <Cloud className="h-8 w-8 text-orangered" />, title: "Cloud Solutions", description: "Leverage the power of cloud computing with managed services." },
{ icon: <Database className="h-8 w-8 text-orangered" />, title: "Database Management", description: "Reliable and secure database solutions for your needs." },
{ icon: <Lock className="h-8 w-8 text-orangered" />, title: "SSL Security", description: "Keep your data safe with industry-standard encryption." }
];
// Pricing plans data - Added actionLink
const plans = [
{
title: "Starter",
description: "Perfect for personal sites.", // Added description for consistency with Card component
price: { currency: "$", amount: "29", period: "mo" },
features: ["1 Website", "10GB Storage", "Free SSL", "24/7 Support", "Basic Analytics"],
popular: false,
actionLink: "/signup?plan=starter" // Link to signup with plan info
},
{
title: "Professional",
description: "Ideal for growing businesses.", // Added description
price: { currency: "$", amount: "79", period: "mo" },
features: ["5 Websites", "50GB Storage", "Free SSL", "Priority Support", "Advanced Analytics", "Free Domain"],
popular: true,
actionLink: "/signup?plan=professional" // Link to signup with plan info
},
{
title: "Enterprise",
description: "Scalable enterprise solutions.", // Added description
price: { currency: "$", amount: "199", period: "mo" },
features: ["Unlimited Websites", "500GB Storage", "Free SSL", "VIP Support", "Advanced Analytics", "Free Domain", "White Label"],
popular: false,
actionLink: "/signup?plan=enterprise" // Link to signup with plan info
}
];
return (
<main>
{/* Hero Section */}
<Hero />
{/* Features Section */}
<section id="home-features" aria-labelledby="features-heading" className="bg-black py-24 sm:py-32">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 id="features-heading" className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-4">
Why Choose <span className="text-orangered">Us</span>
</h2>
<p className="text-lg text-gray-400 max-w-3xl mx-auto">
Experience the perfect blend of innovation, performance, and reliability.
</p>
</div>
{/* Use CardGrid and Card for features */}
<CardGrid columns={{ sm: 1, md: 2, lg: 4 }} gap={8}>
{features.map((feature, index) => (
<Card
key={index}
variant="feature" // Explicitly set variant
icon={feature.icon}
title={feature.title}
description={feature.description}
/>
))}
</CardGrid>
</div>
</section>
{/* Pricing Section */}
<section id="home-pricing" aria-labelledby="pricing-heading" className="bg-gradient-to-b from-black via-gray-900 to-black py-24 sm:py-32">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="text-center mb-16">
<h2 id="pricing-heading" className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white mb-4">
Choose Your <span className="text-orangered">Plan</span>
</h2>
<p className="text-lg text-gray-400 max-w-3xl mx-auto">
Select the perfect plan for your needs with our flexible pricing options.
</p>
</div>
{/* Use CardGrid and Card for pricing */}
<CardGrid columns={{ sm: 1, md: 2, lg: 3 }} gap={8}>
{plans.map((plan, index) => (
<Card
key={index}
{...plan} // Spreads title, description, price, features, popular, actionLink
variant="pricing"
actionLabel="Get Started" // Label for the button/link
// actionLink is now passed via {...plan}
/>
))}
</CardGrid>
</div>
</section>
{/* Optional: Add other sections like Testimonials, Contact CTA, etc. */}
</main>
);
};
export default Home;
// src/pages/Profile.tsx
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
import { useNavigate, Link } from 'react-router-dom'; // Import Link
import { User, Mail, Phone, MapPin, Lock, Upload, Edit2, Check, X, Loader2, Server, Globe /* Add other service icons */ } from 'lucide-react';
import { toast } from 'react-hot-toast';
import { profileSchema, passwordSchema } from '../lib/validation'; // Assuming these are okay
import type { z } from 'zod';
// Corrected import path
import { useAuth } from '../components/AuthContext';
// Assuming ConfirmationModal exists
// import { ConfirmationModal } from '../components/shared/ConfirmationModal';
// --- Define Types (Example) ---
interface UserProfileData {
name: string;
email: string; // Usually not editable directly here
phone: string | null;
address: string | null;
avatarUrl: string | null;
}
interface UserServiceData {
id: string;
name: string;
type: 'hosting' | 'vps' | 'domain' | string; // Allow other types
status: 'active' | 'pending' | 'expired' | string;
expiryDate: string | null; // ISO Date string or null
price: number | null;
manageLink?: string; // Optional link provided by API
}
interface ApiError { message: string; }
type ProfileFormData = z.infer<typeof profileSchema>;
type PasswordFormData = z.infer<typeof passwordSchema>;
// --- Component ---
const Profile: React.FC = () => {
const navigate = useNavigate();
const { user, isLoadingAuth, setUser: setAuthUser } = useAuth(); // Get setUser to update context if needed
// UI State
const [isEditing, setIsEditing] = useState(false);
const [showPasswordChange, setShowPasswordChange] = useState(false);
// Data State
const [userProfile, setUserProfile] = useState<UserProfileData | null>(null);
const [services, setServices] = useState<UserServiceData[]>([]);
// Form State
const [profileForm, setProfileForm] = useState<Partial<ProfileFormData>>({}); // Use partial for editing form
const [passwordForm, setPasswordForm] = useState<PasswordFormData>({ currentPassword: '', newPassword: '', confirmPassword: '' });
// Loading/Error State
const [isLoadingProfile, setIsLoadingProfile] = useState(true);
const [isLoadingServices, setIsLoadingServices] = useState(true);
const [isSubmittingProfile, setIsSubmittingProfile] = useState(false);
const [isSubmittingPassword, setIsSubmittingPassword] = useState(false);
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
const [profileError, setProfileError] = useState<string | null>(null);
const [servicesError, setServicesError] = useState<string | null>(null);
const [formErrors, setFormErrors] = useState<Record<string, string>>({});
const avatarInputRef = useRef<HTMLInputElement>(null);
// --- Get API Base URL & Token ---
const API_BASE_URL = useMemo(() => { /* ... copy ... */ return "/api/v1"; }, []);
const token = useMemo(() => localStorage.getItem('authToken'), []);
// --- API Call Helper ---
const callApi = useCallback(async <T,>(/* ... copy helper ... */
endpoint: string, method: string, body?: object | FormData | null, isFormData = false
): Promise<T> => {
if (!token) throw new Error('Authentication token not found.');
const headers: HeadersInit = { 'Authorization': `Bearer ${token}` };
if (!isFormData && body) headers['Content-Type'] = 'application/json';
const config: RequestInit = { method, headers, body: isFormData ? (body as FormData) : (body ? JSON.stringify(body) : null) };
const response = await fetch(`${API_BASE_URL}${endpoint}`, config);
const data = await response.json();
if (!response.ok) throw new Error((data as ApiError)?.message || `API Error: ${response.status}`);
return data as T;
}, [API_BASE_URL, token]);
// --- Fetch Initial Data ---
const loadData = useCallback(async () => {
if (!user || !token) { // Ensure user and token are available
setIsLoadingProfile(false); setIsLoadingServices(false);
return;
}
setIsLoadingProfile(true); setIsLoadingServices(true);
setProfileError(null); setServicesError(null);
try {
const [profileData, servicesData] = await Promise.all([
callApi<UserProfileData>('/users/profile', 'GET'), // GET /api/users/profile
callApi<UserServiceData[]>('/users/services', 'GET') // GET /api/users/services
]);
setUserProfile(profileData);
setProfileForm({ // Initialize form with fetched data
name: profileData?.name || '',
email: profileData?.email || '', // Keep email display consistent
phone: profileData?.phone || '',
address: profileData?.address || '',
avatar: profileData?.avatarUrl || undefined // Zod schema uses avatar? Let's assume it means avatarUrl
});
setServices(servicesData || []);
} catch (err: any) {
console.error('Failed to load profile/services:', err);
setProfileError(err.message || 'Failed to load profile data.');
setServicesError(err.message || 'Failed to load services.');
} finally {
setIsLoadingProfile(false);
setIsLoadingServices(false);
}
}, [callApi, user, token]);
useEffect(() => {
// Only load data once auth is confirmed and user exists
if (!isLoadingAuth && user) {
loadData();
} else if (!isLoadingAuth && !user) {
// Redirect handled by ProtectedRoute, but clear loading state
setIsLoadingProfile(false);
setIsLoadingServices(false);
}
}, [isLoadingAuth, user, loadData]);
// --- Event Handlers ---
const handleFormChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const { name, value } = e.target;
setProfileForm(prev => ({ ...prev, [name]: value }));
if (formErrors[name]) setFormErrors(prev => ({ ...prev, [name]: '' })); // Clear error on change
};
const handlePasswordFormChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setPasswordForm(prev => ({ ...prev, [name]: value }));
if (formErrors[name]) setFormErrors(prev => ({ ...prev, [name]: '' }));
};
const handleAvatarChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file || !user) return;
if (file.size > 5 * 1024 * 1024) return toast.error('Image size must be less than 5MB');
const formData = new FormData();
formData.append('avatar', file);
setIsUploadingAvatar(true); setProfileError(null);
try {
// POST /api/users/profile/avatar
const result = await callApi<{ avatarUrl: string }>('/users/profile/avatar', 'POST', formData, true);
// Update local profile state and potentially the global user state in AuthContext
setUserProfile(prev => prev ? { ...prev, avatarUrl: result.avatarUrl } : null);
setAuthUser(prev => prev ? { ...prev, avatarUrl: result.avatarUrl } : null); // Update global state if needed
toast.success('Profile picture updated.');
} catch (err: any) {
toast.error(err.message || 'Failed to upload avatar.');
setProfileError(err.message);
} finally {
setIsUploadingAvatar(false);
if (avatarInputRef.current) avatarInputRef.current.value = ''; // Clear file input
}
};
const handleProfileUpdate = async () => {
setIsSubmittingProfile(true); setFormErrors({}); setProfileError(null);
try {
// Validate only the fields being updated (name, phone, address)
const dataToValidate = {
name: profileForm.name,
email: userProfile?.email, // Keep original email for validation if needed by schema
phone: profileForm.phone,
address: profileForm.address
};
const validatedData = profileSchema.parse(dataToValidate);
// PUT /api/users/profile
const updatedProfile = await callApi<UserProfileData>('/users/profile', 'PUT', {
name: validatedData.name,
phone: validatedData.phone,
address: validatedData.address,
// Don't send email or avatar here unless API supports it
});
setUserProfile(updatedProfile); // Update local state with response from API
setAuthUser(prev => prev ? { ...prev, name: updatedProfile.name } : null); // Update global name
setIsEditing(false);
toast.success('Profile updated successfully.');
} catch (error: any) {
if (error instanceof z.ZodError) {
const newErrors: Record<string, string> = {};
error.errors.forEach(err => { if (err.path) newErrors[err.path[0]] = err.message; });
setFormErrors(newErrors);
toast.error("Please fix the errors in the form.");
} else {
toast.error(error.message || 'Failed to update profile.');
setProfileError(error.message);
}
} finally {
setIsSubmittingProfile(false);
}
};
const handlePasswordChange = async (event: React.FormEvent) => {
event.preventDefault();
setIsSubmittingPassword(true); setFormErrors({}); setProfileError(null);
try {
const validatedData = passwordSchema.parse(passwordForm);
// PUT /api/users/password
await callApi('/users/password', 'PUT', {
currentPassword: validatedData.currentPassword,
newPassword: validatedData.newPassword,
});
setShowPasswordChange(false);
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
toast.success('Password updated successfully.');
} catch (error: any) {
if (error instanceof z.ZodError) {
const newErrors: Record<string, string> = {};
error.errors.forEach(err => { if (err.path) newErrors[err.path[0]] = err.message; });
setFormErrors(newErrors);
toast.error("Please fix the errors in the form.");
} else {
toast.error(error.message || 'Failed to update password.');
setProfileError(error.message); // Show error related to password change
}
} finally {
setIsSubmittingPassword(false);
}
};
// --- Helpers ---
const getStatusColor = (status: UserServiceData['status']) => { /* ... copy ... */
switch (status) {
case 'active': return 'bg-green-500';
default: return 'bg-gray-500';
}
};
const formatDate = (dateString: string | null) => { /* ... copy ... */
if (!dateString) return '-';
try { return new Date(dateString).toLocaleDateString(); } catch(e) { return dateString; }
};
// --- Render Loading/Initial State ---
if (isLoadingAuth || isLoadingProfile) {
return <div className="flex justify-center items-center min-h-screen bg-black"><Loader2 className="w-10 h-10 animate-spin text-gray-500" /></div>;
}
// If ProtectedRoute isn't used, this might be needed, but ideally ProtectedRoute handles it
if (!user || !userProfile) {
return <div className="text-center text-gray-500 py-10">Could not load profile data or user not logged in.</div>;
}
// --- Render Profile Page ---
return (
// Removed pt-16, assuming DashboardLayout handles padding if Profile is used there
// If Profile is a standalone page, add pt-16 back
<div className="min-h-screen bg-gradient-to-b from-black via-gray-900 to-black text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 sm:py-16">
{/* Profile Error Display */}
{profileError && (
<div className="mb-6 bg-red-600/10 border border-red-600/30 text-red-400 px-4 py-3 rounded-lg text-sm">
Profile Error: {profileError}
</div>
)}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Profile Card */}
<div className="lg:col-span-1">
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl p-6 sm:p-8 shadow-lg border border-gray-800/50">
{/* Avatar */}
<div className="w-24 h-24 sm:w-32 sm:h-32 mx-auto relative mb-6">
<img
src={userProfile.avatarUrl || `https://ui-avatars.com/api/?name=${encodeURIComponent(userProfile.name || userProfile.email)}&background=random`} // Fallback avatar
alt="Profile"
className="w-full h-full rounded-full object-cover ring-2 ring-gray-700"
/>
<label className="absolute bottom-0 right-0 bg-orangered hover:bg-red-600 text-white p-2 rounded-full cursor-pointer transition-colors focus-within:ring-2 focus-within:ring-offset-2 focus-within:ring-offset-gray-900 focus-within:ring-orangered">
{isUploadingAvatar ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Upload className="h-4 w-4" />
)}
<input ref={avatarInputRef} type="file" className="hidden" accept="image/*" onChange={handleAvatarChange} disabled={isUploadingAvatar || isSubmittingProfile} />
</label>
</div>
{/* Profile Info / Form */}
<div className="space-y-4">
{isEditing ? (
/* Edit Form */
<>
{/* Name */}
<div>
<label htmlFor="name" className="block text-xs font-medium text-gray-400 mb-1">Full Name</label>
<input id="name" name="name" type="text" value={profileForm.name || ''} onChange={handleFormChange} disabled={isSubmittingProfile}
className={`w-full bg-gray-800 border ${formErrors.name ? 'border-red-500' : 'border-gray-700'} rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:ring-1 focus:ring-orangered`} />
{formErrors.name && <p className="mt-1 text-xs text-red-500">{formErrors.name}</p>}
</div>
{/* Email (Display Only) */}
<div>
<label className="block text-xs font-medium text-gray-400 mb-1">Email</label>
<p className="text-sm text-gray-300 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2">{userProfile.email}</p>
</div>
{/* Phone */}
<div>
<label htmlFor="phone" className="block text-xs font-medium text-gray-400 mb-1">Phone</label>
<input id="phone" name="phone" type="tel" value={profileForm.phone || ''} onChange={handleFormChange} disabled={isSubmittingProfile}
className={`w-full bg-gray-800 border ${formErrors.phone ? 'border-red-500' : 'border-gray-700'} rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:ring-1 focus:ring-orangered`} />
{formErrors.phone && <p className="mt-1 text-xs text-red-500">{formErrors.phone}</p>}
</div>
{/* Address */}
<div>
<label htmlFor="address" className="block text-xs font-medium text-gray-400 mb-1">Address</label>
<textarea id="address" name="address" value={profileForm.address || ''} onChange={handleFormChange} disabled={isSubmittingProfile} rows={3}
className={`w-full bg-gray-800 border ${formErrors.address ? 'border-red-500' : 'border-gray-700'} rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:ring-1 focus:ring-orangered`} />
{formErrors.address && <p className="mt-1 text-xs text-red-500">{formErrors.address}</p>}
</div>
{/* Action Buttons */}
<div className="flex space-x-3 pt-2">
<button onClick={handleProfileUpdate} disabled={isSubmittingProfile || isUploadingAvatar}
className="flex-1 bg-orangered hover:bg-red-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center justify-center text-sm font-medium disabled:opacity-50">
{isSubmittingProfile ? <Loader2 className="h-4 w-4 animate-spin" /> : <><Check className="h-4 w-4 mr-1.5" /> Save</>}
</button>
<button onClick={() => { setIsEditing(false); setFormErrors({}); setProfileForm(userProfile || {}); }} disabled={isSubmittingProfile || isUploadingAvatar}
className="flex-1 bg-gray-600 hover:bg-gray-500 text-white px-4 py-2 rounded-lg transition-colors flex items-center justify-center text-sm font-medium disabled:opacity-50">
<X className="h-4 w-4 mr-1.5" /> Cancel
</button>
</div>
</>
) : (
/* Display Mode */
<>
<div className="flex items-center text-gray-300 text-sm"><User className="h-4 w-4 text-gray-500 mr-3 flex-shrink-0" /> {userProfile.name}</div>
<div className="flex items-center text-gray-300 text-sm"><Mail className="h-4 w-4 text-gray-500 mr-3 flex-shrink-0" /> {userProfile.email}</div>
<div className="flex items-center text-gray-300 text-sm"><Phone className="h-4 w-4 text-gray-500 mr-3 flex-shrink-0" /> {userProfile.phone || <span className="text-gray-500 italic">Not set</span>}</div>
<div className="flex items-center text-gray-300 text-sm"><MapPin className="h-4 w-4 text-gray-500 mr-3 flex-shrink-0" /> {userProfile.address || <span className="text-gray-500 italic">Not set</span>}</div>
<button onClick={() => { setIsEditing(true); setProfileForm(userProfile); setFormErrors({}); }} disabled={isUploadingAvatar}
className="w-full mt-4 bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg transition-colors flex items-center justify-center text-sm font-medium disabled:opacity-50">
<Edit2 className="h-4 w-4 mr-1.5" /> Edit Profile
</button>
</>
)}
{/* Change Password Button */}
<button onClick={() => { setShowPasswordChange(true); setFormErrors({}); }} disabled={isSubmittingProfile || isUploadingAvatar}
className="w-full mt-3 bg-gray-700 hover:bg-gray-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center justify-center text-sm font-medium disabled:opacity-50">
<Lock className="h-4 w-4 mr-1.5" /> Change Password
</button>
</div>
</div>
</div>
{/* Services Section */}
<div className="lg:col-span-2">
<div className="bg-gray-900/50 backdrop-blur-sm rounded-xl p-6 sm:p-8 shadow-lg border border-gray-800/50">
<h2 className="text-xl sm:text-2xl font-bold text-white mb-6">Your Services</h2>
{/* Services Error Display */}
{servicesError && (
<div className="mb-4 bg-red-600/10 border border-red-600/30 text-red-400 px-4 py-3 rounded-lg text-sm">
Services Error: {servicesError}
</div>
)}
{/* Services Loading */}
{isLoadingServices && (
<div className="flex justify-center items-center h-20"><Loader2 className="w-6 h-6 animate-spin text-gray-500" /></div>
)}
{/* Services Empty */}
{!isLoadingServices && services.length === 0 && !servicesError && (
<p className="text-gray-500 text-center py-4">You have no active services.</p>
)}
{/* Services List */}
{!isLoadingServices && services.length > 0 && (
<div className="space-y-4">
{services.map((service) => (
<div key={service.id} className="bg-gray-800/50 rounded-lg p-4 sm:p-5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3">
{/* Service Info */}
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="text-base sm:text-lg font-semibold text-white">{service.name}</span>
<span className={`px-2 py-0.5 rounded-full text-xs font-medium text-white ${getStatusColor(service.status)}`}>
{service.status}
</span>
</div>
<div className="mt-1 text-xs sm:text-sm text-gray-400">
Type: {service.type.toUpperCase()} {service.expiryDate && `• Expires: ${formatDate(service.expiryDate)}`}
</div>
</div>
{/* Actions */}
<div className="flex items-center space-x-2 sm:space-x-3 mt-2 sm:mt-0 flex-shrink-0">
{service.price !== null && (
<span className="text-lg font-semibold text-white">
€{service.price.toFixed(2)}<span className="text-xs text-gray-400">/mo</span>
</span>
)}
{/* Use Link if manageLink exists, otherwise button (or hide) */}
{service.manageLink ? (
<Link to={service.manageLink} className="text-sm bg-orangered hover:bg-red-600 text-white px-4 py-1.5 rounded-md transition-colors">Manage</Link>
) : (
<button className="text-sm bg-orangered hover:bg-red-600 text-white px-4 py-1.5 rounded-md transition-colors">Manage</button> // Placeholder
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
</div>
{/* Password Change Modal */}
{showPasswordChange && (
<div className="fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-gray-900 rounded-xl shadow-xl p-6 sm:p-8 max-w-md w-full relative">
<button onClick={() => { setShowPasswordChange(false); setFormErrors({}); setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' }); }} disabled={isSubmittingPassword}
className="absolute top-3 right-3 text-gray-500 hover:text-white transition-colors disabled:opacity-50">
<X className="h-6 w-6" />
</button>
<h3 className="text-xl sm:text-2xl font-bold text-white mb-6 text-center">Change Password</h3>
{/* Password Form */}
<form onSubmit={handlePasswordChange} className="space-y-4">
<div>
<label htmlFor="currentPassword" className="block text-sm font-medium text-gray-400 mb-1">Current Password</label>
<input id="currentPassword" name="currentPassword" type="password" value={passwordForm.currentPassword} onChange={handlePasswordFormChange} disabled={isSubmittingPassword} required
className={`w-full bg-gray-800 border ${formErrors.currentPassword ? 'border-red-500' : 'border-gray-700'} rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:ring-1 focus:ring-orangered`} />
{formErrors.currentPassword && <p className="mt-1 text-xs text-red-500">{formErrors.currentPassword}</p>}
</div>
<div>
<label htmlFor="newPassword" className="block text-sm font-medium text-gray-400 mb-1">New Password</label>
<input id="newPassword" name="newPassword" type="password" value={passwordForm.newPassword} onChange={handlePasswordFormChange} disabled={isSubmittingPassword} required
className={`w-full bg-gray-800 border ${formErrors.newPassword ? 'border-red-500' : 'border-gray-700'} rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:ring-1 focus:ring-orangered`} />
{formErrors.newPassword && <p className="mt-1 text-xs text-red-500">{formErrors.newPassword}</p>}
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-400 mb-1">Confirm New Password</label>
<input id="confirmPassword" name="confirmPassword" type="password" value={passwordForm.confirmPassword} onChange={handlePasswordFormChange} disabled={isSubmittingPassword} required
className={`w-full bg-gray-800 border ${formErrors.confirmPassword ? 'border-red-500' : 'border-gray-700'} rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:ring-1 focus:ring-orangered`} />
{formErrors.confirmPassword && <p className="mt-1 text-xs text-red-500">{formErrors.confirmPassword}</p>}
</div>
{/* Display general password error */}
{profileError && <p className="text-sm text-red-400">{profileError}</p>}
{/* Action Buttons */}
<div className="flex space-x-3 pt-2">
<button type="submit" disabled={isSubmittingPassword}
className="flex-1 bg-orangered hover:bg-red-600 text-white px-4 py-2 rounded-lg transition-colors flex items-center justify-center text-sm font-medium disabled:opacity-50">
{isSubmittingPassword ? <Loader2 className="h-4 w-4 animate-spin" /> : 'Update Password'}
</button>
<button type="button" onClick={() => { setShowPasswordChange(false); setFormErrors({}); setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' }); }} disabled={isSubmittingPassword}
className="flex-1 bg-gray-600 hover:bg-gray-500 text-white px-4 py-2 rounded-lg transition-colors flex items-center justify-center text-sm font-medium disabled:opacity-50">
Cancel
</button>
</div>
</form>
</div>
</div>
)}
</div>
</div>
);
};
export default Profile;
// src/pages/Services.tsx
import React from 'react';
import { Code, Cloud, Server, Database, Archive, Sparkles } from 'lucide-react';
import { Link } from 'react-router-dom';
// Import reusable components
import Card from '../components/Card';
import CardGrid from '../components/CardGrid';
const Services: React.FC = () => { // Added React.FC type
// Add actionLink to make cards clickable
const servicesData = [
{
icon: <Code className="h-10 w-10 sm:h-12 sm:w-12 text-orangered" />,
title: "Custom Development",
description: "WordPress sites, custom HTML, and modern web apps tailored to your needs.",
actionLink: "/websites" // Link to relevant page
},
{
icon: <Cloud className="h-10 w-10 sm:h-12 sm:w-12 text-orangered" />,
title: "Cloud Hosting",
description: "Reliable cloud hosting with high performance, scalability, and 99.9% uptime.",
actionLink: "/hosting#shared-hosting" // Link to relevant page/section
},
{
icon: <Server className="h-10 w-10 sm:h-12 sm:w-12 text-orangered" />,
title: "VPS Hosting",
description: "Powerful Virtual Private Servers with full root access and dedicated resources.",
actionLink: "/hosting#vps-hosting" // Link to relevant page/section
},
{
icon: <Database className="h-10 w-10 sm:h-12 sm:w-12 text-orangered" />,
title: "DNS Management",
description: "Professional DNS management with advanced features for reliability and control.",
actionLink: "/services/dns" // Example link (needs corresponding page/route)
},
{
icon: <Archive className="h-10 w-10 sm:h-12 sm:w-12 text-orangered" />,
title: "Incremental Backup",
description: "Secure incremental backups ensuring your data is always protected and recoverable.",
actionLink: "/services/backup" // Example link
},
{
icon: <Sparkles className="h-10 w-10 sm:h-12 sm:w-12 text-orangered" />,
title: "Custom Solutions",
description: "Tailored hosting and development solutions designed for unique requirements.",
actionLink: "/contact?subject=Custom+Solution" // Link to contact with subject
}
];
return (
// Use main, add pt-16 assuming fixed Navbar
<main className="pt-16 min-h-screen bg-gradient-to-b from-black via-gray-900 to-black text-white">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-16 sm:py-24">
{/* Header Section */}
<section aria-labelledby="services-page-heading" className="text-center mb-12 sm:mb-16">
<h1 id="services-page-heading" className="text-4xl sm:text-5xl font-bold text-white mb-4 sm:mb-6">
Our <span className="text-orangered">Services</span>
</h1>
<p className="text-lg sm:text-xl text-gray-400 max-w-3xl mx-auto">
Comprehensive technology solutions designed to empower your digital presence and drive growth.
</p>
</section>
{/* Services Grid Section */}
<section aria-labelledby="core-services-heading">
<h2 id="core-services-heading" className="sr-only">Core Services</h2> {/* Hidden heading for structure */}
{/* Use CardGrid and Card */}
<CardGrid columns={{ sm: 1, md: 2, lg: 3 }} gap={8}>
{servicesData.map((service, index) => (
<Card
key={index}
variant="feature" // Use feature variant styling
icon={service.icon}
title={service.title}
description={service.description}
actionLink={service.actionLink} // Pass the link to make the card clickable
// The Card component (refactored version) will wrap this in a Link if actionLink is provided
/>
))}
</CardGrid>
</section>
{/* Custom Solution CTA Section */}
<section aria-labelledby="custom-solution-heading" className="mt-16 sm:mt-24 bg-gradient-to-r from-orangered via-red-600 to-red-700 rounded-2xl p-8 sm:p-12 text-center shadow-xl">
<h2 id="custom-solution-heading" className="text-3xl font-bold text-white mb-4">
Need a Custom Solution?
</h2>
<p className="text-white/90 mb-8 max-w-2xl mx-auto text-lg">
Our experts are ready to discuss your specific requirements and craft the perfect technology solution for your business challenges.
</p>
<Link
to="/contact?subject=Custom+Solution" // Pre-fill subject?
className="inline-block bg-white text-orangered px-8 py-3 rounded-lg font-semibold hover:bg-gray-200 transition-colors shadow-lg focus:outline-none focus-visible:ring-2 focus-visible:ring-white" // Added focus style
>
Get in Touch
</Link>
</section>
</div>
</main>
);
};
export default Services;
// src/pages/Signup.tsx
import React, { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom'; // Import Link
import { Mail, Lock, User, ArrowLeft, Loader2, AlertTriangle } from 'lucide-react';
// Corrected import path
import { useAuth } from '../components/AuthContext';
import { z } from 'zod';
import { toast } from 'react-hot-toast'; // Import toast for API errors
// Zod schema (keep as is, it's good)
const signupSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain one uppercase letter')
.regex(/[a-z]/, 'Must contain one lowercase letter')
.regex(/[0-9]/, 'Must contain one number')
.regex(/[^A-Za-z0-9]/, 'Must contain one special character'),
confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ["confirmPassword"],
});
type SignupFormData = z.infer<typeof signupSchema>;
const Signup: React.FC = () => { // Added React.FC type
const navigate = useNavigate();
// Use correct hook, get isSubmitting state if needed (though signup handles its own)
const { signup, isSubmitting: isAuthSubmitting } = useAuth(); // Renamed isLoading to isSubmitting
const [formErrors, setFormErrors] = useState<Partial<Record<keyof SignupFormData, string>>>({});
const [submitError, setSubmitError] = useState<string | null>(null); // For API errors
const [formData, setFormData] = useState<SignupFormData>({
name: '', email: '', password: '', confirmPassword: ''
});
// Input change handler
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { id, value } = e.target;
setFormData(prev => ({ ...prev, [id]: value }));
// Clear validation error on change
if (formErrors[id as keyof SignupFormData]) {
setFormErrors(prev => ({ ...prev, [id]: undefined }));
}
setSubmitError(null); // Clear general submit error
};
// Form submission handler
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setFormErrors({});
setSubmitError(null);
// No need for local isLoading if using isAuthSubmitting from context
// --- 1. Validation ---
const validationResult = signupSchema.safeParse(formData);
if (!validationResult.success) {
const fieldErrors: Partial<Record<keyof SignupFormData, string>> = {};
validationResult.error.errors.forEach(err => {
if (err.path.length > 0) {
fieldErrors[err.path[0] as keyof SignupFormData] = err.message;
}
});
setFormErrors(fieldErrors);
toast.error("Please fix the errors in the form.");
return;
}
// --- 2. API Call via Auth Context ---
try {
// Destructure confirmPassword, as signup function doesn't need it
const { confirmPassword, ...signupData } = validationResult.data;
// Call the signup function from context
await signup(signupData.email, signupData.password, signupData.name);
// SUCCESS: signup function handles toast & navigation to /login
// No need to navigate('/dashboard') here.
// Optionally clear form, though navigation will unmount it
// setFormData({ name: '', email: '', password: '', confirmPassword: '' });
} catch (error: any) {
// --- 3. Handle API Errors ---
console.error("Signup API error:", error);
const message = error.message || 'Failed to create account. Please try again.';
setSubmitError(message); // Display error within the form
toast.error(message); // Also show a toast
}
// No finally block needed if using context's isSubmitting
};
return (
<div className="min-h-screen bg-gradient-to-b from-black via-gray-900 to-black flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-md w-full space-y-8 bg-gray-900/50 backdrop-blur-sm p-6 sm:p-8 rounded-xl border border-gray-800/50 shadow-xl">
<div>
{/* Back Button */}
<button
onClick={() => navigate('/')} // Or navigate(-1) to go back
className="flex items-center text-sm text-gray-400 hover:text-white transition-colors mb-6 focus:outline-none focus-visible:ring-2 focus-visible:ring-gray-500 rounded"
aria-label="Back to Home"
>
<ArrowLeft className="h-4 w-4 mr-1.5" />
Back to Home
</button>
{/* Header */}
<h2 className="text-2xl sm:text-3xl font-bold text-white text-center">
Create Your Account
</h2>
<p className="mt-2 text-center text-sm text-gray-400">
Join us and experience premium hosting services.
</p>
</div>
{/* Signup Form */}
<form className="mt-8 space-y-5" onSubmit={handleSubmit}>
{/* General Submit Error */}
{submitError && (
<div className="bg-red-600/10 border border-red-600/30 text-red-400 px-4 py-2 rounded-lg text-sm">
<AlertTriangle className="inline w-4 h-4 mr-2" /> {submitError}
</div>
)}
{/* Inputs */}
<div className="space-y-4 rounded-md shadow-sm">
{/* Name */}
<div>
<label htmlFor="name" className="block text-sm font-medium text-gray-400 mb-1.5">Full Name</label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-500 pointer-events-none" />
<input id="name" type="text" value={formData.name} onChange={handleInputChange} required disabled={isAuthSubmitting}
className={`w-full bg-gray-800 border ${formErrors.name ? 'border-red-500' : 'border-gray-700'} rounded-lg pl-10 pr-4 py-2.5 text-white focus:outline-none focus:ring-1 ${formErrors.name ? 'focus:ring-red-500' : 'focus:ring-orangered'} disabled:opacity-50`}
placeholder="John Doe" />
</div>
{formErrors.name && <p className="mt-1 text-xs text-red-400">{formErrors.name}</p>}
</div>
{/* Email */}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-400 mb-1.5">Email Address</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-500 pointer-events-none" />
<input id="email" type="email" value={formData.email} onChange={handleInputChange} required disabled={isAuthSubmitting}
className={`w-full bg-gray-800 border ${formErrors.email ? 'border-red-500' : 'border-gray-700'} rounded-lg pl-10 pr-4 py-2.5 text-white focus:outline-none focus:ring-1 ${formErrors.email ? 'focus:ring-red-500' : 'focus:ring-orangered'} disabled:opacity-50`}
placeholder="[email protected]" />
</div>
{formErrors.email && <p className="mt-1 text-xs text-red-400">{formErrors.email}</p>}
</div>
{/* Password */}
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-400 mb-1.5">Password</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-500 pointer-events-none" />
<input id="password" type="password" value={formData.password} onChange={handleInputChange} required disabled={isAuthSubmitting}
className={`w-full bg-gray-800 border ${formErrors.password ? 'border-red-500' : 'border-gray-700'} rounded-lg pl-10 pr-4 py-2.5 text-white focus:outline-none focus:ring-1 ${formErrors.password ? 'focus:ring-red-500' : 'focus:ring-orangered'} disabled:opacity-50`}
placeholder="••••••••" />
</div>
{formErrors.password && <p className="mt-1 text-xs text-red-400">{formErrors.password}</p>}
</div>
{/* Confirm Password */}
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-400 mb-1.5">Confirm Password</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-gray-500 pointer-events-none" />
<input id="confirmPassword" type="password" value={formData.confirmPassword} onChange={handleInputChange} required disabled={isAuthSubmitting}
className={`w-full bg-gray-800 border ${formErrors.confirmPassword ? 'border-red-500' : 'border-gray-700'} rounded-lg pl-10 pr-4 py-2.5 text-white focus:outline-none focus:ring-1 ${formErrors.confirmPassword ? 'focus:ring-red-500' : 'focus:ring-orangered'} disabled:opacity-50`}
placeholder="••••••••" />
</div>
{formErrors.confirmPassword && <p className="mt-1 text-xs text-red-400">{formErrors.confirmPassword}</p>}
</div>
</div>
{/* Submit Button */}
<button type="submit" disabled={isAuthSubmitting}
className="w-full mt-6 flex justify-center items-center bg-orangered hover:bg-red-600 text-white px-4 py-2.5 rounded-lg transition-colors font-semibold disabled:opacity-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-gray-900 focus-visible:ring-orangered">
{isAuthSubmitting ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
'Create Account'
)}
</button>
{/* Link to Login */}
<p className="text-center text-sm text-gray-400 pt-2">
Already have an account?{' '}
{/* Use Link component for navigation */}
<Link
to="/login"
className="font-medium text-orangered hover:text-red-600 transition-colors focus:outline-none focus-visible:underline"
>
Log In
</Link>
</p>
</form>
</div>
</div>
);
};
export default Signup;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment