Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,183 @@
// File: src/pages/Auth/Login.jsx
import React, { useState } from "react";
import { useNavigate } from "react-router";
import { useAuth } from "../../services/Services";
/**
* Login - User login page
*
* Complete implementation with MaplePress backend integration
*/
function Login() {
const navigate = useNavigate();
const { authManager } = useAuth();
const [formData, setFormData] = useState({
email: "",
password: "",
});
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
setError("");
};
const handleSubmit = async (e) => {
e.preventDefault();
setError("");
setLoading(true);
try {
// Login via AuthManager
await authManager.login(formData.email, formData.password);
// Navigate to dashboard on success
navigate("/dashboard");
} catch (err) {
setError(err.message || "Login failed. Please try again.");
} finally {
setLoading(false);
}
};
const handleRegisterClick = () => {
navigate("/register");
};
const handleBackClick = () => {
navigate("/");
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex items-center justify-center p-4">
<div className="max-w-md w-full">
{/* Logo/Brand */}
<div className="text-center mb-8">
<div className="inline-flex items-center gap-2 bg-white px-6 py-3 rounded-full shadow-lg mb-4">
<span className="text-3xl">🍁</span>
<h1 className="text-2xl font-bold bg-gradient-to-r from-indigo-600 to-blue-600 bg-clip-text text-transparent">
MaplePress
</h1>
</div>
</div>
{/* Login Card */}
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
{/* Header */}
<div className="mb-8">
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Welcome back
</h2>
<p className="text-gray-600">Sign in to your account to continue</p>
</div>
{/* Error Message */}
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
{/* Login Form */}
<form onSubmit={handleSubmit} className="space-y-6">
{/* Email Field */}
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-2"
>
Email Address
</label>
<input
type="email"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
required
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2
focus:ring-indigo-500 focus:border-transparent transition-colors"
placeholder="you@example.com"
/>
</div>
{/* Password Field */}
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 mb-2"
>
Password
</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleInputChange}
required
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2
focus:ring-indigo-500 focus:border-transparent transition-colors"
placeholder="••••••••"
/>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={loading}
className="w-full px-4 py-3 bg-gradient-to-r from-indigo-600 to-blue-600 text-white font-semibold rounded-xl
hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200
disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
Signing in...
</span>
) : (
"Sign In"
)}
</button>
</form>
{/* Register Link */}
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
Don't have an account?{" "}
<button
onClick={handleRegisterClick}
className="text-indigo-600 font-semibold hover:text-indigo-700 hover:underline"
>
Create one now
</button>
</p>
</div>
{/* Back to Home Link */}
<div className="mt-4 pt-4 border-t border-gray-100 text-center">
<button
onClick={handleBackClick}
className="text-sm text-gray-500 hover:text-gray-700 transition-colors inline-flex items-center gap-1"
>
<span></span>
<span>Back to Home</span>
</button>
</div>
</div>
</div>
</div>
);
}
export default Login;

View file

@ -0,0 +1,529 @@
// File: src/pages/Auth/Register.jsx
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router";
import { useAuth } from "../../services/Services";
import RegisterService from "../../services/API/RegisterService";
/**
* Register - User registration page
*
* Complete implementation with all required fields for MaplePress backend
*/
function Register() {
const navigate = useNavigate();
const { authManager } = useAuth();
const [formData, setFormData] = useState({
// User fields
first_name: "",
last_name: "",
email: "",
password: "",
confirmPassword: "",
// Tenant/Organization fields
tenant_name: "",
// Consent fields
agree_terms_of_service: false,
agree_promotions: false,
agree_to_tracking_across_third_party_apps_and_services: false,
});
const [errors, setErrors] = useState({}); // Field-specific errors
const [generalError, setGeneralError] = useState(""); // General error message
const [loading, setLoading] = useState(false);
const handleInputChange = (e) => {
const { name, value, type, checked } = e.target;
setFormData((prev) => ({
...prev,
[name]: type === "checkbox" ? checked : value,
}));
// Clear errors when user types
setErrors((prev) => ({ ...prev, [name]: null }));
setGeneralError("");
};
/**
* Format field name for display in error messages
* Converts snake_case to Title Case
*/
const formatFieldName = (fieldName) => {
const fieldLabels = {
first_name: "First Name",
last_name: "Last Name",
email: "Email",
password: "Password",
confirm_password: "Confirm Password",
confirmPassword: "Confirm Password",
tenant_name: "Organization Name",
agree_terms_of_service: "Terms of Service",
agree_promotions: "Promotions",
agree_to_tracking_across_third_party_apps_and_services: "Third-Party Tracking",
};
return fieldLabels[fieldName] || fieldName;
};
/**
* Parse backend error (RFC 9457 format)
* Backend returns structured validation errors in format:
* {
* type: "about:blank",
* title: "Validation Error",
* status: 400,
* detail: "One or more validation errors occurred",
* errors: {
* email: ["Invalid email format"],
* password: ["Password is required", "Password must be at least 8 characters"]
* }
* }
*/
const parseBackendError = (error) => {
console.log('[Register] Parsing error:', error);
console.log('[Register] error.validationErrors:', error.validationErrors);
console.log('[Register] error.message:', error.message);
const fieldErrors = {};
let generalError = null;
// Map backend field names (snake_case) to frontend field names (camelCase)
const fieldNameMap = {
'confirm_password': 'confirmPassword',
'first_name': 'first_name',
'last_name': 'last_name',
'tenant_name': 'tenant_name',
'agree_terms_of_service': 'agree_terms_of_service',
'agree_promotions': 'agree_promotions',
'agree_to_tracking_across_third_party_apps_and_services': 'agree_to_tracking_across_third_party_apps_and_services',
};
// Check if error has RFC 9457 validation errors structure
if (error.validationErrors && typeof error.validationErrors === 'object') {
console.log('[Register] Found RFC 9457 validation errors');
// Process each field's errors
Object.entries(error.validationErrors).forEach(([backendFieldName, errorMessages]) => {
if (Array.isArray(errorMessages) && errorMessages.length > 0) {
// Map backend field name to frontend field name
const frontendFieldName = fieldNameMap[backendFieldName] || backendFieldName;
// Join multiple error messages for the same field
fieldErrors[frontendFieldName] = errorMessages.join('; ');
console.log(`[Register] Field error: ${backendFieldName} -> ${frontendFieldName} = ${fieldErrors[frontendFieldName]}`);
}
});
// Use the detail as general error if available
if (error.message) {
generalError = error.message;
}
} else {
console.log('[Register] No RFC 9457 errors found, using fallback');
// Fallback for non-RFC 9457 errors or legacy format
generalError = error.message || "An error occurred. Please try again.";
}
console.log('[Register] Parsed result:', { fieldErrors, generalError });
return {
fieldErrors,
generalError
};
};
const handleSubmit = async (e) => {
e.preventDefault();
setErrors({});
setGeneralError("");
setLoading(true);
try {
// Prepare registration data
// All validation (including password matching) is now handled by backend
const registrationData = {
email: formData.email,
password: formData.password,
confirmPassword: formData.confirmPassword,
first_name: formData.first_name,
last_name: formData.last_name,
tenant_name: formData.tenant_name,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC",
agree_terms_of_service: formData.agree_terms_of_service,
agree_promotions: formData.agree_promotions,
agree_to_tracking_across_third_party_apps_and_services: formData.agree_to_tracking_across_third_party_apps_and_services,
};
// Register via AuthManager
await authManager.register(registrationData);
// Navigate to dashboard on success
navigate("/dashboard");
} catch (err) {
console.log('[Register] Caught error in handleSubmit:', err);
console.log('[Register] Error type:', typeof err);
console.log('[Register] Error properties:', Object.keys(err));
// Parse RFC 9457 error response
const { fieldErrors, generalError } = parseBackendError(err);
console.log('[Register] Setting errors:', fieldErrors);
console.log('[Register] Setting generalError:', generalError);
setErrors(fieldErrors);
if (generalError) {
setGeneralError(generalError);
}
} finally {
setLoading(false);
}
};
const handleLoginClick = () => {
navigate("/login");
};
const handleBackClick = () => {
navigate("/");
};
/**
* Get className for input field with error highlighting
*/
const getInputClassName = (fieldName, baseClassName) => {
const hasError = errors[fieldName];
if (hasError) {
return baseClassName.replace('border-gray-300', 'border-red-500') + ' ring-1 ring-red-500';
}
return baseClassName;
};
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex items-center justify-center p-4 py-12">
<div className="max-w-2xl w-full">
{/* Logo/Brand */}
<div className="text-center mb-8">
<div className="inline-flex items-center gap-2 bg-white px-6 py-3 rounded-full shadow-lg mb-4">
<span className="text-3xl">🍁</span>
<h1 className="text-2xl font-bold bg-gradient-to-r from-indigo-600 to-blue-600 bg-clip-text text-transparent">
MaplePress
</h1>
</div>
</div>
{/* Register Card */}
<div className="bg-white rounded-2xl shadow-xl p-8 border border-gray-100">
{/* Header */}
<div className="mb-6">
<h2 className="text-2xl font-bold text-gray-900 mb-2">
Create your account
</h2>
<p className="text-gray-600">Get started with MaplePress in minutes</p>
</div>
{/* Error Summary Box */}
{(generalError || Object.entries(errors).filter(([_, msg]) => msg).length > 0) && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 text-red-600 text-xl"></div>
<div className="flex-1">
<h3 className="text-sm font-semibold text-red-800 mb-2">
Please correct the following errors:
</h3>
<ul className="space-y-1 text-sm text-red-600">
{generalError && (
<li> {generalError}</li>
)}
{Object.entries(errors).filter(([_, message]) => message).map(([field, message]) => (
<li key={field}>
<span className="font-medium">{formatFieldName(field)}:</span> {message}
</li>
))}
</ul>
</div>
</div>
</div>
)}
{/* Registration Form */}
<form onSubmit={handleSubmit} className="space-y-5">
{/* Personal Information Section */}
<div className="space-y-4">
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide">
Personal Information
</h3>
{/* First Name */}
<div>
<label
htmlFor="first_name"
className="block text-sm font-medium text-gray-700 mb-1"
>
First Name *
</label>
<input
type="text"
id="first_name"
name="first_name"
value={formData.first_name}
onChange={handleInputChange}
className={getInputClassName("first_name", "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-colors")}
placeholder="John"
/>
{errors.first_name && (
<p className="mt-1 text-sm text-red-600">{errors.first_name}</p>
)}
</div>
{/* Last Name */}
<div>
<label
htmlFor="last_name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Last Name *
</label>
<input
type="text"
id="last_name"
name="last_name"
value={formData.last_name}
onChange={handleInputChange}
className={getInputClassName("last_name", "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-colors")}
placeholder="Doe"
/>
{errors.last_name && (
<p className="mt-1 text-sm text-red-600">{errors.last_name}</p>
)}
</div>
{/* Email */}
<div>
<label
htmlFor="email"
className="block text-sm font-medium text-gray-700 mb-1"
>
Email Address *
</label>
<input
type="text"
id="email"
name="email"
value={formData.email}
onChange={handleInputChange}
className={getInputClassName("email", "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-colors")}
placeholder="you@example.com"
/>
{errors.email && (
<p className="mt-1 text-sm text-red-600">{errors.email}</p>
)}
</div>
{/* Password */}
<div>
<label
htmlFor="password"
className="block text-sm font-medium text-gray-700 mb-1"
>
Password *
</label>
<input
type="password"
id="password"
name="password"
value={formData.password}
onChange={handleInputChange}
className={getInputClassName("password", "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-colors")}
placeholder="••••••••"
/>
{errors.password ? (
<p className="mt-1 text-sm text-red-600">{errors.password}</p>
) : (
<p className="mt-1 text-xs text-gray-500">
Minimum 8 characters
</p>
)}
</div>
{/* Confirm Password */}
<div>
<label
htmlFor="confirmPassword"
className="block text-sm font-medium text-gray-700 mb-1"
>
Confirm Password *
</label>
<input
type="password"
id="confirmPassword"
name="confirmPassword"
value={formData.confirmPassword}
onChange={handleInputChange}
className={getInputClassName("confirmPassword", "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-colors")}
placeholder="••••••••"
/>
{errors.confirmPassword && (
<p className="mt-1 text-sm text-red-600">{errors.confirmPassword}</p>
)}
</div>
</div>
{/* Organization Information Section */}
<div className="space-y-4 pt-4 border-t border-gray-200">
<h3 className="text-sm font-semibold text-gray-900 uppercase tracking-wide">
Organization Information
</h3>
{/* Organization Name */}
<div>
<label
htmlFor="tenant_name"
className="block text-sm font-medium text-gray-700 mb-1"
>
Organization Name *
</label>
<input
type="text"
id="tenant_name"
name="tenant_name"
value={formData.tenant_name}
onChange={handleInputChange}
className={getInputClassName("tenant_name", "w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent transition-colors")}
placeholder="Acme Corporation"
/>
{errors.tenant_name && (
<p className="mt-1 text-sm text-red-600">{errors.tenant_name}</p>
)}
<p className="mt-1 text-xs text-gray-500">
Your organization's URL will be automatically generated
</p>
</div>
</div>
{/* Terms and Consent Section */}
<div className="space-y-3 pt-4 border-t border-gray-200">
{/* Terms of Service */}
<div>
<div className="flex items-start">
<input
type="checkbox"
id="agree_terms_of_service"
name="agree_terms_of_service"
checked={formData.agree_terms_of_service}
onChange={handleInputChange}
className={`mt-1 h-4 w-4 text-indigo-600 rounded focus:ring-indigo-500 ${errors.agree_terms_of_service ? 'border-red-500' : 'border-gray-300'}`}
/>
<label
htmlFor="agree_terms_of_service"
className="ml-2 text-sm text-gray-700"
>
I agree to the{" "}
<a
href="#"
className="text-indigo-600 hover:text-indigo-700 font-medium"
>
Terms of Service
</a>{" "}
*
</label>
</div>
{errors.agree_terms_of_service && (
<p className="mt-1 ml-6 text-sm text-red-600">{errors.agree_terms_of_service}</p>
)}
</div>
{/* Promotional Emails (Optional) */}
<div className="flex items-start">
<input
type="checkbox"
id="agree_promotions"
name="agree_promotions"
checked={formData.agree_promotions}
onChange={handleInputChange}
className="mt-1 h-4 w-4 text-indigo-600 border-gray-300 rounded
focus:ring-indigo-500"
/>
<label
htmlFor="agree_promotions"
className="ml-2 text-sm text-gray-700"
>
Send me promotional emails and updates (optional)
</label>
</div>
{/* Third-Party Tracking (Optional) */}
<div className="flex items-start">
<input
type="checkbox"
id="agree_to_tracking_across_third_party_apps_and_services"
name="agree_to_tracking_across_third_party_apps_and_services"
checked={formData.agree_to_tracking_across_third_party_apps_and_services}
onChange={handleInputChange}
className="mt-1 h-4 w-4 text-indigo-600 border-gray-300 rounded
focus:ring-indigo-500"
/>
<label
htmlFor="agree_to_tracking_across_third_party_apps_and_services"
className="ml-2 text-sm text-gray-700"
>
Allow tracking across third-party apps and services for analytics (optional)
</label>
</div>
</div>
{/* Submit Button */}
<button
type="submit"
disabled={loading}
className="w-full px-4 py-3 bg-gradient-to-r from-indigo-600 to-blue-600 text-white font-semibold rounded-xl
hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200
disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none mt-6"
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none"/>
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"/>
</svg>
Creating account...
</span>
) : (
"Create Account"
)}
</button>
</form>
{/* Login Link */}
<div className="mt-6 text-center">
<p className="text-sm text-gray-600">
Already have an account?{" "}
<button
onClick={handleLoginClick}
className="text-indigo-600 font-semibold hover:text-indigo-700 hover:underline"
>
Sign in instead
</button>
</p>
</div>
{/* Back to Home Link */}
<div className="mt-4 pt-4 border-t border-gray-100 text-center">
<button
onClick={handleBackClick}
className="text-sm text-gray-500 hover:text-gray-700 transition-colors inline-flex items-center gap-1"
>
<span></span>
<span>Back to Home</span>
</button>
</div>
</div>
</div>
</div>
);
}
export default Register;

View file

@ -0,0 +1,368 @@
// File: src/pages/Dashboard/Dashboard.jsx
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router";
import { useAuth } from "../../services/Services";
import SiteService from "../../services/API/SiteService";
/**
* Dashboard - Launch page for managing WordPress sites
*/
function Dashboard() {
const navigate = useNavigate();
const { authManager } = useAuth();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [sites, setSites] = useState([]);
const [sitesLoading, setSitesLoading] = useState(false);
const [sitesError, setSitesError] = useState(null);
useEffect(() => {
console.log("[Dashboard] 🔒 Checking authentication...");
// Wait for AuthManager to initialize before checking authentication
const checkAuth = async () => {
const isInitialized = authManager.getIsInitialized();
console.log("[Dashboard] 🔧 AuthManager initialization status:", {
isInitialized,
});
if (!isInitialized) {
console.log("[Dashboard] ⏳ Waiting for AuthManager to initialize...");
// Check again in 50ms
setTimeout(checkAuth, 50);
return;
}
// Now safe to check authentication
const isAuth = authManager.isAuthenticated();
console.log("[Dashboard] 🔍 Authentication check result:", {
isAuthenticated: isAuth,
hasAccessToken: !!authManager.getAccessToken(),
hasUser: !!authManager.getUser(),
});
if (!isAuth) {
console.log("[Dashboard] ⚠️ Not authenticated, redirecting to login");
setIsLoading(false);
navigate("/login");
return;
}
// Proactively refresh token if needed BEFORE showing dashboard
try {
console.log("[Dashboard] 🔄 Ensuring token is valid...");
await authManager.ensureValidToken();
console.log("[Dashboard] ✅ Token is valid and ready");
} catch (error) {
console.error("[Dashboard] ❌ Token refresh failed on mount:", error);
// Token refresh failed, redirect to login
setIsLoading(false);
navigate("/login");
return;
}
// Get user data
const userData = authManager.getUser();
console.log("[Dashboard] ✅ User authenticated, loading user data:", {
email: userData?.email,
role: userData?.role,
});
setUser(userData);
setIsLoading(false);
};
// Start the authentication check
checkAuth();
}, [authManager, navigate]);
// Load sites after authentication is complete
useEffect(() => {
if (!user || isLoading) {
return;
}
const loadSites = async () => {
setSitesLoading(true);
setSitesError(null);
console.log("[Dashboard] 📋 Loading sites...");
try {
const response = await SiteService.listSites({ pageSize: 50 });
console.log("[Dashboard] ✅ Sites loaded:", response.sites.length);
setSites(response.sites);
} catch (error) {
console.error("[Dashboard] ❌ Failed to load sites:", error);
setSitesError(error.message || "Failed to load sites");
} finally {
setSitesLoading(false);
}
};
loadSites();
}, [user, isLoading]);
// Background token refresh - check every 60 seconds
// The AuthManager will only refresh if token expires within 1 minute
useEffect(() => {
console.log("[Dashboard] 🔁 Setting up background token refresh (every 60s)");
const refreshInterval = setInterval(async () => {
if (!authManager.getIsInitialized()) {
return;
}
// Check if we're still authenticated (refresh token still valid)
if (!authManager.isAuthenticated()) {
console.warn("[Dashboard] ⚠️ Refresh token expired, redirecting to login");
clearInterval(refreshInterval);
navigate("/login");
return;
}
try {
// This will refresh the token if it's expiring soon (within 1 minute)
await authManager.ensureValidToken();
} catch (error) {
console.error("[Dashboard] ❌ Background token refresh failed:", error);
// Token refresh failed, redirect to login
clearInterval(refreshInterval);
navigate("/login");
}
}, 60000); // Check every 60 seconds (access token expires in 15 minutes)
return () => {
console.log("[Dashboard] 🛑 Cleaning up background token refresh");
clearInterval(refreshInterval);
};
}, [authManager, navigate]);
const handleLogout = async () => {
await authManager.logout();
navigate("/");
};
// Show loading state while waiting for authentication check
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-600 border-t-transparent"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100">
{/* Navigation Bar */}
<nav className="bg-white border-b border-gray-200 shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<div className="flex items-center gap-2">
<span className="text-2xl">🍁</span>
<h1 className="text-xl font-bold bg-gradient-to-r from-indigo-600 to-blue-600 bg-clip-text text-transparent">
MaplePress
</h1>
</div>
{/* User Menu */}
<div className="flex items-center gap-4">
<div className="hidden sm:flex items-center gap-2 px-4 py-2 bg-gray-50 rounded-lg">
<div className="w-8 h-8 bg-gradient-to-r from-indigo-600 to-blue-600 rounded-full
flex items-center justify-center text-white text-sm font-semibold">
{user?.email?.[0]?.toUpperCase() || "U"}
</div>
<span className="text-sm font-medium text-gray-700">
{user?.email || "User"}
</span>
</div>
<button
onClick={handleLogout}
className="px-4 py-2 text-sm font-medium text-gray-700 hover:text-gray-900
border border-gray-300 rounded-lg hover:bg-gray-50 transition-all"
>
Sign Out
</button>
</div>
</div>
</div>
</nav>
{/* Main Content */}
<main className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Welcome Section */}
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
Welcome to MaplePress
</h1>
<p className="text-xl text-gray-600">
Cloud services platform for your WordPress sites
</p>
</div>
{/* Sites Section */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden mb-8">
{/* Section Header */}
<div className="bg-gradient-to-r from-indigo-600 to-blue-600 px-6 py-4 flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-2xl">🌐</span>
<h2 className="text-xl font-bold text-white">Your WordPress Sites</h2>
</div>
<button
onClick={() => navigate("/sites/add")}
className="px-4 py-2 bg-white text-indigo-600 rounded-lg hover:bg-indigo-50
transition-all font-medium text-sm flex items-center gap-2"
>
<span></span>
<span>Add Site</span>
</button>
</div>
{/* Loading State */}
{sitesLoading && (
<div className="p-12 text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-600 border-t-transparent"></div>
<p className="mt-4 text-gray-600">Loading your sites...</p>
</div>
)}
{/* Error State */}
{!sitesLoading && sitesError && (
<div className="p-12 text-center">
<div className="w-20 h-20 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
<span className="text-4xl"></span>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">
Failed to Load Sites
</h3>
<p className="text-gray-600 mb-6 max-w-md mx-auto">{sitesError}</p>
<button
onClick={() => window.location.reload()}
className="px-6 py-3 bg-gradient-to-r from-indigo-600 to-blue-600 text-white
rounded-lg hover:from-indigo-700 hover:to-blue-700 transition-all
font-medium shadow-lg hover:shadow-xl"
>
Retry
</button>
</div>
)}
{/* Empty State */}
{!sitesLoading && !sitesError && sites.length === 0 && (
<div className="p-12 text-center">
<div className="w-20 h-20 bg-gradient-to-br from-indigo-100 to-blue-100 rounded-full
flex items-center justify-center mx-auto mb-6">
<span className="text-4xl">🚀</span>
</div>
<h3 className="text-xl font-semibold text-gray-900 mb-3">
No sites connected yet
</h3>
<p className="text-gray-600 mb-6 max-w-md mx-auto">
Get started by connecting your first WordPress site to unlock cloud-powered search,
analytics, and more.
</p>
<button
onClick={() => navigate("/sites/add")}
className="px-6 py-3 bg-gradient-to-r from-indigo-600 to-blue-600 text-white
rounded-lg hover:from-indigo-700 hover:to-blue-700 transition-all
font-medium shadow-lg hover:shadow-xl"
>
Connect Your First Site
</button>
</div>
)}
{/* Site List */}
{!sitesLoading && !sitesError && sites.length > 0 && (
<div className="divide-y divide-gray-100">
{sites.map((site) => (
<div
key={site.id}
className="p-6 hover:bg-gray-50 transition-all flex items-center justify-between"
>
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-indigo-500 to-blue-500 rounded-lg
flex items-center justify-center text-white font-bold text-lg">
{site.domain[0]?.toUpperCase() || "W"}
</div>
<div>
<h3 className="font-semibold text-gray-900 flex items-center gap-2">
{site.domain}
{site.isVerified && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
Verified
</span>
)}
{!site.isVerified && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800">
Pending
</span>
)}
</h3>
<p className="text-sm text-gray-600">
Status: {site.status} Added {site.createdAt.toLocaleDateString()}
</p>
</div>
</div>
<button
onClick={() => navigate(`/sites/${site.id}`)}
className="px-4 py-2 text-sm font-medium text-indigo-600 hover:text-indigo-700
border border-indigo-200 rounded-lg hover:bg-indigo-50 transition-all"
>
Manage
</button>
</div>
))}
</div>
)}
</div>
{/* Getting Started Guide */}
<div className="bg-gradient-to-r from-indigo-50 to-blue-50 rounded-2xl p-8
border border-indigo-100">
<div className="flex items-start gap-4">
<div className="text-4xl">💡</div>
<div className="flex-1">
<h3 className="text-xl font-semibold text-gray-900 mb-3">
Getting Started
</h3>
<p className="text-gray-700 mb-4">
To connect your WordPress site to MaplePress:
</p>
<ol className="space-y-2 text-gray-700 mb-6">
<li className="flex items-start gap-2">
<span className="font-semibold text-indigo-600">1.</span>
<span>Install the MaplePress plugin on your WordPress site</span>
</li>
<li className="flex items-start gap-2">
<span className="font-semibold text-indigo-600">2.</span>
<span>Enter your API credentials from the plugin settings</span>
</li>
<li className="flex items-start gap-2">
<span className="font-semibold text-indigo-600">3.</span>
<span>Your site will appear here once connected</span>
</li>
</ol>
<div className="flex gap-3">
<button className="px-4 py-2 bg-indigo-600 text-white rounded-lg
hover:bg-indigo-700 transition-all font-medium">
Download Plugin
</button>
<button className="px-4 py-2 text-indigo-600 border border-indigo-200 rounded-lg
hover:bg-white transition-all font-medium">
View Documentation
</button>
</div>
</div>
</div>
</div>
</main>
</div>
);
}
export default Dashboard;

View file

@ -0,0 +1,279 @@
// File: src/pages/Home/IndexPage.jsx
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router";
import HealthService from "../../services/API/HealthService";
/**
* IndexPage - Home page for MaplePress
*
* Modern landing page with improved design and health status monitoring
*/
function IndexPage() {
const navigate = useNavigate();
const [healthStatus, setHealthStatus] = useState({
checking: true,
healthy: false,
responseTime: null,
});
// Check backend health on mount
useEffect(() => {
let isMounted = true;
const checkBackendHealth = async () => {
try {
const status = await HealthService.getDetailedStatus();
if (isMounted) {
setHealthStatus({
checking: false,
healthy: status.healthy,
responseTime: status.responseTimeMs,
});
}
} catch (error) {
if (isMounted) {
setHealthStatus({
checking: false,
healthy: false,
responseTime: null,
});
}
}
};
// Initial check
checkBackendHealth();
// Re-check every 30 seconds
const intervalId = setInterval(checkBackendHealth, 30000);
return () => {
isMounted = false;
clearInterval(intervalId);
};
}, []);
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100">
{/* Health Status Badge - Fixed position top right */}
<div className="fixed top-4 right-4 z-50">
{healthStatus.checking ? (
<div className="bg-white rounded-full px-4 py-2 shadow-lg flex items-center gap-2 border border-gray-200">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-pulse"></div>
<span className="text-sm font-medium text-gray-600">
Checking status...
</span>
</div>
) : healthStatus.healthy ? (
<div className="bg-white rounded-full px-4 py-2 shadow-lg flex items-center gap-2 border border-green-200">
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
<span className="text-sm font-medium text-gray-700">
All systems operational
</span>
{healthStatus.responseTime && (
<span className="text-xs text-gray-500">
({healthStatus.responseTime}ms)
</span>
)}
</div>
) : (
<div className="bg-white rounded-full px-4 py-2 shadow-lg flex items-center gap-2 border border-red-200">
<div className="w-2 h-2 bg-red-500 rounded-full"></div>
<span className="text-sm font-medium text-gray-700">
Service unavailable
</span>
</div>
)}
</div>
{/* Hero Section */}
<div className="container mx-auto px-4 py-16 md:py-24">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="text-center mb-16">
<div className="inline-block mb-6">
<div className="flex items-center gap-3 bg-white px-6 py-3 rounded-full shadow-lg">
<span className="text-4xl">🍁</span>
<h1 className="text-3xl md:text-4xl font-bold bg-gradient-to-r from-indigo-600 to-blue-600 bg-clip-text text-transparent">
MaplePress
</h1>
</div>
</div>
<h2 className="text-4xl md:text-6xl font-bold text-gray-900 mb-6 leading-tight">
Cloud Services for
<br />
<span className="bg-gradient-to-r from-indigo-600 to-blue-600 bg-clip-text text-transparent">
WordPress Sites
</span>
</h2>
<p className="text-xl md:text-2xl text-gray-600 mb-8 max-w-3xl mx-auto">
Supercharge your WordPress with cloud-powered search, analytics,
and processing. Keep your site fast while adding powerful
features.
</p>
{/* CTA Buttons */}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center mb-12">
<button
onClick={() => navigate("/register")}
className="group relative w-full sm:w-auto px-8 py-4 bg-gradient-to-r from-indigo-600 to-blue-600
text-white font-semibold rounded-xl shadow-lg hover:shadow-xl
transform hover:-translate-y-0.5 transition-all duration-200"
>
<span className="relative z-10">Get Started Free</span>
<div
className="absolute inset-0 bg-gradient-to-r from-indigo-700 to-blue-700 rounded-xl opacity-0
group-hover:opacity-100 transition-opacity"
></div>
</button>
<button
onClick={() => navigate("/login")}
className="w-full sm:w-auto px-8 py-4 bg-white text-gray-700 font-semibold rounded-xl
border-2 border-gray-200 hover:border-indigo-300 hover:bg-gray-50
shadow-md hover:shadow-lg transform hover:-translate-y-0.5 transition-all duration-200"
>
Sign In
</button>
</div>
{/* Trust Badge */}
<p className="text-sm text-gray-500">
Open source No credit card required Free tier available
</p>
</div>
{/* Features Grid */}
<div className="grid md:grid-cols-3 gap-8 mb-16">
{/* Feature 1 */}
<div
className="group bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl
transform hover:-translate-y-1 transition-all duration-200"
>
<div
className="w-16 h-16 bg-gradient-to-br from-indigo-500 to-blue-500 rounded-2xl
flex items-center justify-center text-3xl mb-6 shadow-lg
group-hover:scale-110 transition-transform"
>
🔍
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">
Cloud-Powered Search
</h3>
<p className="text-gray-600 leading-relaxed">
Lightning-fast search with advanced filtering, powered by cloud
infrastructure. Offload processing from your WordPress server.
</p>
</div>
{/* Feature 2 */}
<div
className="group bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl
transform hover:-translate-y-1 transition-all duration-200"
>
<div
className="w-16 h-16 bg-gradient-to-br from-purple-500 to-pink-500 rounded-2xl
flex items-center justify-center text-3xl mb-6 shadow-lg
group-hover:scale-110 transition-transform"
>
📊
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">
Analytics & Insights
</h3>
<p className="text-gray-600 leading-relaxed">
Deep insights into content performance, search patterns, and
user behavior. Make data-driven decisions.
</p>
</div>
{/* Feature 3 */}
<div
className="group bg-white rounded-2xl p-8 shadow-lg hover:shadow-xl
transform hover:-translate-y-1 transition-all duration-200"
>
<div
className="w-16 h-16 bg-gradient-to-br from-green-500 to-emerald-500 rounded-2xl
flex items-center justify-center text-3xl mb-6 shadow-lg
group-hover:scale-110 transition-transform"
>
</div>
<h3 className="text-xl font-bold text-gray-900 mb-3">
Peak Performance
</h3>
<p className="text-gray-600 leading-relaxed">
Keep your WordPress site blazing fast by offloading heavy tasks
to the cloud. Better experience for your users.
</p>
</div>
</div>
{/* How it Works */}
<div className="bg-white rounded-3xl p-8 md:p-12 shadow-xl mb-16">
<h3 className="text-3xl font-bold text-gray-900 text-center mb-12">
Simple Setup in 3 Steps
</h3>
<div className="grid md:grid-cols-3 gap-8">
<div className="text-center">
<div
className="w-16 h-16 bg-indigo-100 text-indigo-600 rounded-full
flex items-center justify-center text-2xl font-bold mx-auto mb-4"
>
1
</div>
<h4 className="font-semibold text-gray-900 mb-2">
Create Account
</h4>
<p className="text-sm text-gray-600">
Sign up in seconds and create your organization
</p>
</div>
<div className="text-center">
<div
className="w-16 h-16 bg-indigo-100 text-indigo-600 rounded-full
flex items-center justify-center text-2xl font-bold mx-auto mb-4"
>
2
</div>
<h4 className="font-semibold text-gray-900 mb-2">
Install Plugin
</h4>
<p className="text-sm text-gray-600">
Add our WordPress plugin and connect your site
</p>
</div>
<div className="text-center">
<div
className="w-16 h-16 bg-indigo-100 text-indigo-600 rounded-full
flex items-center justify-center text-2xl font-bold mx-auto mb-4"
>
3
</div>
<h4 className="font-semibold text-gray-900 mb-2">Go Live</h4>
<p className="text-sm text-gray-600">
Enable cloud features and enjoy better performance
</p>
</div>
</div>
</div>
{/* Footer */}
<div className="text-center">
<p className="text-gray-500 mb-2">
Part of the{" "}
<span className="font-semibold">Maple Open Technologies</span>{" "}
open-source software suite
</p>
<p className="text-sm text-gray-400">
Built with for the WordPress community
</p>
</div>
</div>
</div>
</div>
);
}
export default IndexPage;

View file

@ -0,0 +1,383 @@
// File: src/pages/Sites/AddSite.jsx
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router";
import { useAuth } from "../../services/Services";
import SiteService from "../../services/API/SiteService";
/**
* AddSite - Page for connecting a new WordPress site to MaplePress
*/
function AddSite() {
const navigate = useNavigate();
const { authManager } = useAuth();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [formData, setFormData] = useState({
site_url: "",
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [generalError, setGeneralError] = useState("");
useEffect(() => {
// Wait for AuthManager to initialize before checking authentication
const checkAuth = () => {
const isInitialized = authManager.getIsInitialized();
if (!isInitialized) {
setTimeout(checkAuth, 50);
return;
}
const isAuth = authManager.isAuthenticated();
if (!isAuth) {
navigate("/login");
return;
}
const userData = authManager.getUser();
setUser(userData);
setIsLoading(false);
};
checkAuth();
}, [authManager, navigate]);
// Background token refresh - check every 30 seconds
useEffect(() => {
const refreshInterval = setInterval(async () => {
if (!authManager.getIsInitialized()) {
return;
}
try {
// This will refresh the token if it's expiring soon (within 1 minute)
await authManager.ensureValidToken();
} catch (error) {
console.error("[AddSite] Background token refresh failed:", error);
// Token refresh failed, redirect to login
navigate("/login");
}
}, 30000); // Check every 30 seconds
return () => clearInterval(refreshInterval);
}, [authManager, navigate]);
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
// Clear error for this field when user starts typing
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: null }));
}
if (generalError) {
setGeneralError("");
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setErrors({});
setGeneralError("");
console.log("[AddSite] Form submitted:", { site_url: formData.site_url });
try {
// Call SiteService to create site
// Backend will extract domain from siteUrl
const response = await SiteService.createSite({
siteUrl: formData.site_url,
});
console.log("[AddSite] Site created successfully:", response);
// Navigate to success page with site data
navigate("/sites/add-success", {
state: {
siteData: {
id: response.id,
domain: response.domain,
site_url: response.siteUrl,
api_key: response.apiKey,
verification_token: response.verificationToken,
verification_instructions: response.verificationInstructions,
status: response.status,
search_index_name: response.searchIndexName,
}
}
});
} catch (error) {
console.error("[AddSite] Site creation failed:", error);
setIsSubmitting(false);
// Parse RFC 9457 validation errors
const { fieldErrors, generalError: parsedGeneralError } = parseBackendError(error);
if (Object.keys(fieldErrors).length > 0) {
setErrors(fieldErrors);
}
setGeneralError(parsedGeneralError || "Failed to create site. Please try again.");
}
};
/**
* Format field names for display
*/
const formatFieldName = (fieldName) => {
const fieldLabels = {
site_url: "Site URL",
};
return fieldLabels[fieldName] || fieldName;
};
/**
* Parse backend RFC 9457 error format
*/
const parseBackendError = (error) => {
console.log("[AddSite] 🔍 Parsing backend error:", error);
console.log("[AddSite] 🔍 Error has validationErrors:", !!error.validationErrors);
console.log("[AddSite] 🔍 validationErrors content:", error.validationErrors);
const fieldErrors = {};
let generalError = null;
// Check if error has RFC 9457 validation errors structure
if (error.validationErrors && typeof error.validationErrors === 'object') {
console.log("[AddSite] ✅ Processing RFC 9457 validation errors");
// Process each field's errors
Object.entries(error.validationErrors).forEach(([fieldName, errorMessages]) => {
if (Array.isArray(errorMessages) && errorMessages.length > 0) {
// Join multiple error messages for the same field with semicolon
fieldErrors[fieldName] = errorMessages.join('; ');
console.log(`[AddSite] 📋 Field "${fieldName}": ${fieldErrors[fieldName]}`);
}
});
// Use the detail/message as general error if available
if (error.message) {
generalError = error.message;
}
} else {
// Fallback for non-RFC 9457 errors
console.log("[AddSite] ⚠️ Non-RFC 9457 error, using fallback");
generalError = error.message || "An error occurred. Please try again.";
}
console.log("[AddSite] 📊 Parsed errors:", { fieldErrors, generalError });
return { fieldErrors, generalError };
};
const handleCancel = () => {
navigate("/dashboard");
};
// Show loading state while waiting for authentication check
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-600 border-t-transparent"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
);
}
// Show form to create site
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100">
{/* Navigation Bar */}
<nav className="bg-white border-b border-gray-200 shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<button
onClick={() => navigate("/dashboard")}
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
>
<span className="text-2xl">🍁</span>
<h1 className="text-xl font-bold bg-gradient-to-r from-indigo-600 to-blue-600 bg-clip-text text-transparent">
MaplePress
</h1>
</button>
{/* User Menu */}
<div className="flex items-center gap-2 px-4 py-2 bg-gray-50 rounded-lg">
<div className="w-8 h-8 bg-gradient-to-r from-indigo-600 to-blue-600 rounded-full
flex items-center justify-center text-white text-sm font-semibold">
{user?.email?.[0]?.toUpperCase() || "U"}
</div>
<span className="text-sm font-medium text-gray-700">
{user?.email || "User"}
</span>
</div>
</div>
</div>
</nav>
{/* Main Content */}
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Header */}
<div className="mb-8">
<button
onClick={handleCancel}
className="text-indigo-600 hover:text-indigo-700 font-medium mb-4 flex items-center gap-2"
>
<span></span>
<span>Back to Dashboard</span>
</button>
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Add WordPress Site
</h1>
<p className="text-gray-600">
Register your WordPress site to generate API credentials
</p>
</div>
{/* Form Card */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
<div className="p-8">
{/* Error Summary Box */}
{(generalError || Object.entries(errors).filter(([_, msg]) => msg).length > 0) && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 text-red-600 text-xl"></div>
<div className="flex-1">
<h3 className="text-sm font-semibold text-red-800 mb-2">
Please correct the following errors:
</h3>
<ul className="space-y-1 text-sm text-red-600">
{generalError && (
<li> {generalError}</li>
)}
{/* Merge domain and site_url errors since they're about the same field */}
{(() => {
const fieldErrors = Object.entries(errors).filter(([_, message]) => message);
const mergedErrors = new Map();
fieldErrors.forEach(([field, message]) => {
const displayName = formatFieldName(field);
if (mergedErrors.has(displayName)) {
// Combine messages if they're different
const existing = mergedErrors.get(displayName);
if (existing !== message) {
mergedErrors.set(displayName, `${existing}; ${message}`);
}
} else {
mergedErrors.set(displayName, message);
}
});
return Array.from(mergedErrors.entries()).map(([displayName, message]) => (
<li key={displayName}>
<span className="font-medium">{displayName}:</span> {message}
</li>
));
})()}
</ul>
</div>
</div>
</div>
)}
<form onSubmit={handleSubmit}>
{/* WordPress Site URL */}
<div className="mb-6">
<label
htmlFor="site_url"
className="block text-sm font-medium text-gray-700 mb-2"
>
WordPress Site URL
</label>
<input
type="text"
id="site_url"
name="site_url"
value={formData.site_url}
onChange={handleInputChange}
className={`w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent
${errors.site_url ? "border-red-500" : "border-gray-300"}`}
placeholder="https://example.com"
disabled={isSubmitting}
autoFocus
/>
{errors.site_url && (
<p className="mt-1 text-sm text-red-600">{errors.site_url}</p>
)}
<p className="mt-1 text-sm text-gray-500">
Enter the full URL of your WordPress site (e.g., https://example.com or https://www.example.com/blog)
</p>
</div>
{/* Info Box */}
<div className="mb-6 p-4 bg-indigo-50 border border-indigo-100 rounded-lg">
<div className="flex items-start gap-3">
<span className="text-2xl">💡</span>
<div className="flex-1">
<h4 className="font-semibold text-gray-900 mb-2">
What Happens Next?
</h4>
<ul className="space-y-1 text-sm text-gray-700">
<li> MaplePress will generate an API key for your site</li>
<li> You'll receive the API key <strong>only once</strong> - save it immediately!</li>
<li> Use the API key in your WordPress plugin settings to connect</li>
</ul>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-3">
<button
type="submit"
disabled={isSubmitting}
className="flex-1 px-6 py-3 bg-gradient-to-r from-indigo-600 to-blue-600 text-white
rounded-lg hover:from-indigo-700 hover:to-blue-700 transition-all
font-medium shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? "Creating Site..." : "Create Site"}
</button>
<button
type="button"
onClick={handleCancel}
disabled={isSubmitting}
className="px-6 py-3 text-gray-700 border border-gray-300 rounded-lg
hover:bg-gray-50 transition-all font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancel
</button>
</div>
</form>
</div>
</div>
{/* Help Section */}
<div className="mt-8 bg-gradient-to-r from-indigo-50 to-blue-50 rounded-2xl p-6 border border-indigo-100">
<div className="flex items-start gap-3">
<span className="text-2xl">📚</span>
<div className="flex-1">
<h3 className="font-semibold text-gray-900 mb-2">
Need the WordPress Plugin?
</h3>
<p className="text-sm text-gray-700 mb-3">
Make sure you have the MaplePress plugin installed on your WordPress site before connecting.
</p>
<button className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700
transition-all font-medium text-sm">
Download Plugin
</button>
</div>
</div>
</div>
</main>
</div>
);
}
export default AddSite;

View file

@ -0,0 +1,543 @@
// File: src/pages/Sites/AddSite.jsx
import React, { useEffect, useState } from "react";
import { useNavigate } from "react-router";
import { useAuth } from "../../services/Services";
import SiteService from "../../services/API/SiteService";
/**
* AddSite - Page for connecting a new WordPress site to MaplePress
*/
function AddSite() {
const navigate = useNavigate();
const { authManager } = useAuth();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [formData, setFormData] = useState({
site_url: "",
});
const [errors, setErrors] = useState({});
const [isSubmitting, setIsSubmitting] = useState(false);
const [generalError, setGeneralError] = useState("");
useEffect(() => {
// Wait for AuthManager to initialize before checking authentication
const checkAuth = () => {
const isInitialized = authManager.getIsInitialized();
if (!isInitialized) {
setTimeout(checkAuth, 50);
return;
}
const isAuth = authManager.isAuthenticated();
if (!isAuth) {
navigate("/login");
return;
}
const userData = authManager.getUser();
setUser(userData);
setIsLoading(false);
};
checkAuth();
}, [authManager, navigate]);
// Background token refresh - check every 30 seconds
useEffect(() => {
const refreshInterval = setInterval(async () => {
if (!authManager.getIsInitialized()) {
return;
}
try {
// This will refresh the token if it's expiring soon (within 1 minute)
await authManager.ensureValidToken();
} catch (error) {
console.error("[AddSite] Background token refresh failed:", error);
// Token refresh failed, redirect to login
navigate("/login");
}
}, 30000); // Check every 30 seconds
return () => clearInterval(refreshInterval);
}, [authManager, navigate]);
const handleInputChange = (e) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
// Clear error for this field when user starts typing
if (errors[name]) {
setErrors((prev) => ({ ...prev, [name]: null }));
}
if (generalError) {
setGeneralError("");
}
};
/**
* Extract domain from URL
*/
const extractDomainFromUrl = (url) => {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (error) {
// If URL is invalid, return it as-is and let backend validation handle it
return url;
}
};
const handleSubmit = async (e) => {
e.preventDefault();
setIsSubmitting(true);
setErrors({});
setGeneralError("");
// Extract domain from the site URL
const domain = extractDomainFromUrl(formData.site_url);
console.log("[AddSite] Form submitted:", { site_url: formData.site_url, extracted_domain: domain });
try {
// Call SiteService to create site
// Send data as-is, let backend validate
const response = await SiteService.createSite({
domain: domain,
siteUrl: formData.site_url,
});
console.log("[AddSite] Site created successfully:", response);
// Navigate to success page with site data
navigate("/sites/add-success", {
state: {
siteData: {
id: response.id,
domain: response.domain,
site_url: response.siteUrl,
api_key: response.apiKey,
verification_token: response.verificationToken,
status: response.status,
search_index_name: response.searchIndexName,
}
}
});
} catch (error) {
console.error("[AddSite] Site creation failed:", error);
setIsSubmitting(false);
// Parse RFC 9457 validation errors
const { fieldErrors, generalError: parsedGeneralError } = parseBackendError(error);
if (Object.keys(fieldErrors).length > 0) {
setErrors(fieldErrors);
}
setGeneralError(parsedGeneralError || "Failed to create site. Please try again.");
}
};
/**
* Format field names for display
*/
const formatFieldName = (fieldName) => {
const fieldLabels = {
domain: "Domain",
site_url: "Site URL",
};
return fieldLabels[fieldName] || fieldName;
};
/**
* Parse backend RFC 9457 error format
*/
const parseBackendError = (error) => {
console.log("[AddSite] 🔍 Parsing backend error:", error);
console.log("[AddSite] 🔍 Error has validationErrors:", !!error.validationErrors);
console.log("[AddSite] 🔍 validationErrors content:", error.validationErrors);
const fieldErrors = {};
let generalError = null;
// Check if error has RFC 9457 validation errors structure
if (error.validationErrors && typeof error.validationErrors === 'object') {
console.log("[AddSite] ✅ Processing RFC 9457 validation errors");
// Process each field's errors
Object.entries(error.validationErrors).forEach(([fieldName, errorMessages]) => {
if (Array.isArray(errorMessages) && errorMessages.length > 0) {
// Join multiple error messages for the same field with semicolon
fieldErrors[fieldName] = errorMessages.join('; ');
console.log(`[AddSite] 📋 Field "${fieldName}": ${fieldErrors[fieldName]}`);
}
});
// Use the detail/message as general error if available
if (error.message) {
generalError = error.message;
}
} else {
// Fallback for non-RFC 9457 errors
console.log("[AddSite] ⚠️ Non-RFC 9457 error, using fallback");
generalError = error.message || "An error occurred. Please try again.";
}
console.log("[AddSite] 📊 Parsed errors:", { fieldErrors, generalError });
return { fieldErrors, generalError };
};
const handleCancel = () => {
navigate("/dashboard");
};
// Show loading state while waiting for authentication check
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-600 border-t-transparent"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
);
}
// Show success page with API key if site was created
if (createdSite) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100">
{/* Navigation Bar */}
<nav className="bg-white border-b border-gray-200 shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<button
onClick={() => navigate("/dashboard")}
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
>
<span className="text-2xl">🍁</span>
<h1 className="text-xl font-bold bg-gradient-to-r from-indigo-600 to-blue-600 bg-clip-text text-transparent">
MaplePress
</h1>
</button>
{/* User Menu */}
<div className="flex items-center gap-2 px-4 py-2 bg-gray-50 rounded-lg">
<div className="w-8 h-8 bg-gradient-to-r from-indigo-600 to-blue-600 rounded-full
flex items-center justify-center text-white text-sm font-semibold">
{user?.email?.[0]?.toUpperCase() || "U"}
</div>
<span className="text-sm font-medium text-gray-700">
{user?.email || "User"}
</span>
</div>
</div>
</div>
</nav>
{/* Success Content */}
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
{/* Success Header */}
<div className="bg-gradient-to-r from-green-600 to-emerald-600 px-8 py-6 text-white">
<div className="flex items-center gap-3 mb-2">
<span className="text-3xl"></span>
<h1 className="text-2xl font-bold">Site Created Successfully!</h1>
</div>
<p className="text-green-50">
Your WordPress site has been registered with MaplePress
</p>
</div>
<div className="p-8">
{/* Site Details */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Site Details</h3>
<div className="space-y-3 bg-gray-50 rounded-lg p-4">
<div>
<span className="text-sm text-gray-600">Domain:</span>
<p className="font-medium text-gray-900">{createdSite.domain}</p>
</div>
<div>
<span className="text-sm text-gray-600">Site URL:</span>
<p className="font-medium text-gray-900">{createdSite.site_url}</p>
</div>
<div>
<span className="text-sm text-gray-600">Status:</span>
<p className="font-medium text-yellow-600 capitalize">{createdSite.status}</p>
</div>
</div>
</div>
{/* API Key Section - IMPORTANT */}
<div className="mb-8 p-6 bg-yellow-50 border-2 border-yellow-200 rounded-lg">
<div className="flex items-start gap-3 mb-4">
<span className="text-2xl"></span>
<div>
<h3 className="text-lg font-bold text-gray-900 mb-1">
Save Your API Key Now!
</h3>
<p className="text-sm text-gray-700">
This API key will <strong>only be shown once</strong>. Copy it now and add it to your WordPress plugin settings.
</p>
</div>
</div>
<div className="bg-white rounded-lg p-4 border border-yellow-300">
<label className="block text-sm font-medium text-gray-700 mb-2">
API Key
</label>
<div className="flex gap-2">
<input
type="text"
value={createdSite.api_key}
readOnly
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg bg-gray-50 font-mono text-sm"
/>
<button
onClick={handleCopyApiKey}
className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700
transition-all font-medium"
>
Copy
</button>
</div>
</div>
</div>
{/* Next Steps */}
<div className="mb-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Next Steps</h3>
<ol className="space-y-3">
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-indigo-600 text-white rounded-full
flex items-center justify-center text-sm font-bold">
1
</span>
<div>
<p className="font-medium text-gray-900">Install the MaplePress WordPress Plugin</p>
<p className="text-sm text-gray-600">Download and activate the plugin on your WordPress site</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-indigo-600 text-white rounded-full
flex items-center justify-center text-sm font-bold">
2
</span>
<div>
<p className="font-medium text-gray-900">Enter Your API Key</p>
<p className="text-sm text-gray-600">
Go to Settings MaplePress and paste your API key (copied above)
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-indigo-600 text-white rounded-full
flex items-center justify-center text-sm font-bold">
3
</span>
<div>
<p className="font-medium text-gray-900">Verify Your Site</p>
<p className="text-sm text-gray-600">
The plugin will automatically verify your site and activate cloud services
</p>
</div>
</li>
</ol>
</div>
{/* Action Button */}
<button
onClick={handleDone}
className="w-full px-6 py-3 bg-gradient-to-r from-indigo-600 to-blue-600 text-white
rounded-lg hover:from-indigo-700 hover:to-blue-700 transition-all
font-medium shadow-lg hover:shadow-xl"
>
Go to Dashboard
</button>
</div>
</div>
</main>
</div>
);
}
// Show form to create site
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100">
{/* Navigation Bar */}
<nav className="bg-white border-b border-gray-200 shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<button
onClick={() => navigate("/dashboard")}
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
>
<span className="text-2xl">🍁</span>
<h1 className="text-xl font-bold bg-gradient-to-r from-indigo-600 to-blue-600 bg-clip-text text-transparent">
MaplePress
</h1>
</button>
{/* User Menu */}
<div className="flex items-center gap-2 px-4 py-2 bg-gray-50 rounded-lg">
<div className="w-8 h-8 bg-gradient-to-r from-indigo-600 to-blue-600 rounded-full
flex items-center justify-center text-white text-sm font-semibold">
{user?.email?.[0]?.toUpperCase() || "U"}
</div>
<span className="text-sm font-medium text-gray-700">
{user?.email || "User"}
</span>
</div>
</div>
</div>
</nav>
{/* Main Content */}
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Header */}
<div className="mb-8">
<button
onClick={handleCancel}
className="text-indigo-600 hover:text-indigo-700 font-medium mb-4 flex items-center gap-2"
>
<span></span>
<span>Back to Dashboard</span>
</button>
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Add WordPress Site
</h1>
<p className="text-gray-600">
Register your WordPress site to generate API credentials
</p>
</div>
{/* Form Card */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
<div className="p-8">
{/* Error Summary Box */}
{(generalError || Object.entries(errors).filter(([_, msg]) => msg).length > 0) && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 text-red-600 text-xl"></div>
<div className="flex-1">
<h3 className="text-sm font-semibold text-red-800 mb-2">
Please correct the following errors:
</h3>
<ul className="space-y-1 text-sm text-red-600">
{generalError && (
<li> {generalError}</li>
)}
{Object.entries(errors).filter(([_, message]) => message).map(([field, message]) => (
<li key={field}>
<span className="font-medium">{formatFieldName(field)}:</span> {message}
</li>
))}
</ul>
</div>
</div>
</div>
)}
<form onSubmit={handleSubmit}>
{/* WordPress Site URL */}
<div className="mb-6">
<label
htmlFor="site_url"
className="block text-sm font-medium text-gray-700 mb-2"
>
WordPress Site URL
</label>
<input
type="text"
id="site_url"
name="site_url"
value={formData.site_url}
onChange={handleInputChange}
className={`w-full px-4 py-3 border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-transparent
${errors.site_url ? "border-red-500" : "border-gray-300"}`}
placeholder="https://example.com"
disabled={isSubmitting}
autoFocus
/>
{errors.site_url && (
<p className="mt-1 text-sm text-red-600">{errors.site_url}</p>
)}
{errors.domain && (
<p className="mt-1 text-sm text-red-600">{errors.domain}</p>
)}
<p className="mt-1 text-sm text-gray-500">
Enter the full URL of your WordPress site (e.g., https://example.com or https://www.example.com/blog)
</p>
</div>
{/* Info Box */}
<div className="mb-6 p-4 bg-indigo-50 border border-indigo-100 rounded-lg">
<div className="flex items-start gap-3">
<span className="text-2xl">💡</span>
<div className="flex-1">
<h4 className="font-semibold text-gray-900 mb-2">
What Happens Next?
</h4>
<ul className="space-y-1 text-sm text-gray-700">
<li> MaplePress will generate an API key for your site</li>
<li> You'll receive the API key <strong>only once</strong> - save it immediately!</li>
<li> Use the API key in your WordPress plugin settings to connect</li>
</ul>
</div>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-3">
<button
type="submit"
disabled={isSubmitting}
className="flex-1 px-6 py-3 bg-gradient-to-r from-indigo-600 to-blue-600 text-white
rounded-lg hover:from-indigo-700 hover:to-blue-700 transition-all
font-medium shadow-lg hover:shadow-xl disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSubmitting ? "Creating Site..." : "Create Site"}
</button>
<button
type="button"
onClick={handleCancel}
disabled={isSubmitting}
className="px-6 py-3 text-gray-700 border border-gray-300 rounded-lg
hover:bg-gray-50 transition-all font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancel
</button>
</div>
</form>
</div>
</div>
{/* Help Section */}
<div className="mt-8 bg-gradient-to-r from-indigo-50 to-blue-50 rounded-2xl p-6 border border-indigo-100">
<div className="flex items-start gap-3">
<span className="text-2xl">📚</span>
<div className="flex-1">
<h3 className="font-semibold text-gray-900 mb-2">
Need the WordPress Plugin?
</h3>
<p className="text-sm text-gray-700 mb-3">
Make sure you have the MaplePress plugin installed on your WordPress site before connecting.
</p>
<button className="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700
transition-all font-medium text-sm">
Download Plugin
</button>
</div>
</div>
</div>
</main>
</div>
);
}
export default AddSite;

View file

@ -0,0 +1,336 @@
// File: src/pages/Sites/AddSiteSuccess.jsx
import React, { useEffect, useState } from "react";
import { useNavigate, useLocation } from "react-router";
import { useAuth } from "../../services/Services";
/**
* AddSiteSuccess - Success page after creating a WordPress site
* Shows the API key (ONLY ONCE) with prominent warning to save it
*/
function AddSiteSuccess() {
const navigate = useNavigate();
const location = useLocation();
const { authManager } = useAuth();
const [copied, setCopied] = useState(false);
const [copiedToken, setCopiedToken] = useState(false);
// Get site data from navigation state
const siteData = location.state?.siteData;
useEffect(() => {
// If no site data, redirect back to add site page
if (!siteData) {
navigate("/sites/add");
return;
}
// Check authentication
const checkAuth = () => {
const isInitialized = authManager.getIsInitialized();
if (!isInitialized) {
setTimeout(checkAuth, 50);
return;
}
const isAuth = authManager.isAuthenticated();
if (!isAuth) {
navigate("/login");
}
};
checkAuth();
}, [authManager, navigate, siteData]);
const handleCopyApiKey = () => {
navigator.clipboard.writeText(siteData.api_key);
setCopied(true);
setTimeout(() => setCopied(false), 3000);
};
const handleCopyVerificationToken = () => {
const txtRecord = `maplepress-verify=${siteData.verification_token}`;
navigator.clipboard.writeText(txtRecord);
setCopiedToken(true);
setTimeout(() => setCopiedToken(false), 3000);
};
const handleDone = () => {
navigate("/dashboard");
};
if (!siteData) {
return null;
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100">
{/* Navigation Bar */}
<nav className="bg-white border-b border-gray-200 shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<button
onClick={() => navigate("/dashboard")}
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
>
<span className="text-2xl">🍁</span>
<h1 className="text-xl font-bold bg-gradient-to-r from-indigo-600 to-blue-600 bg-clip-text text-transparent">
MaplePress
</h1>
</button>
</div>
</div>
</nav>
{/* Main Content */}
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Success Header */}
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 bg-green-100 rounded-full mb-4">
<span className="text-3xl"></span>
</div>
<h1 className="text-3xl font-bold text-gray-900 mb-2">
Site Created Successfully!
</h1>
<p className="text-gray-600">
Your WordPress site has been registered with MaplePress
</p>
</div>
{/* Critical Warning Box */}
<div className="mb-8 p-6 bg-red-50 border-2 border-red-300 rounded-lg">
<div className="flex items-start gap-4">
<span className="text-4xl flex-shrink-0"></span>
<div className="flex-1">
<h2 className="text-xl font-bold text-red-900 mb-2">
IMPORTANT: Save Your API Key Now!
</h2>
<div className="space-y-2 text-red-800">
<p className="font-semibold">
This API key will <span className="underline">ONLY be shown once</span> and cannot be retrieved later.
</p>
<p>
If you lose this key, you will need to generate a new one, which will invalidate the current key.
</p>
</div>
</div>
</div>
</div>
{/* Site Information Card */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden mb-6">
<div className="p-8">
{/* Site Details */}
<div className="mb-6">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Site Details</h3>
<div className="space-y-3">
<div>
<span className="text-sm text-gray-600">Domain:</span>
<p className="font-medium text-gray-900">{siteData.domain}</p>
</div>
<div>
<span className="text-sm text-gray-600">Site URL:</span>
<p className="font-medium text-gray-900">{siteData.site_url}</p>
</div>
<div>
<span className="text-sm text-gray-600">Status:</span>
<p className="font-medium text-yellow-600 capitalize">{siteData.status}</p>
</div>
</div>
</div>
{/* API Key Section */}
<div className="mb-6 p-6 bg-yellow-50 border-2 border-yellow-200 rounded-lg">
<h3 className="text-lg font-bold text-gray-900 mb-4">
Your API Key
</h3>
<div className="bg-white rounded-lg p-4 border border-yellow-300 mb-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
API Key (copy this now!)
</label>
<div className="flex gap-2">
<input
type="text"
value={siteData.api_key}
readOnly
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg bg-gray-50 font-mono text-sm"
/>
<button
onClick={handleCopyApiKey}
className={`px-6 py-3 rounded-lg font-medium transition-all
${copied
? "bg-green-600 text-white"
: "bg-indigo-600 text-white hover:bg-indigo-700"
}`}
>
{copied ? "✓ Copied!" : "Copy"}
</button>
</div>
</div>
<p className="text-sm text-gray-700">
<strong>Remember:</strong> Store this API key in a secure location like a password manager.
You'll need it to configure the MaplePress WordPress plugin.
</p>
</div>
</div>
</div>
{/* DNS Verification Section */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden mb-6">
<div className="p-8">
<div className="flex items-start gap-4 mb-6">
<span className="text-4xl flex-shrink-0">🔐</span>
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
Verify Domain Ownership
</h3>
<p className="text-gray-600 text-sm">
To activate your site, you need to prove you own the domain by adding a DNS TXT record.
This is the same method used by Google Search Console and other major services.
</p>
</div>
</div>
{/* DNS TXT Record */}
<div className="mb-6 p-6 bg-blue-50 border-2 border-blue-200 rounded-lg">
<h4 className="text-md font-bold text-gray-900 mb-3">
Add this DNS TXT record to {siteData.domain}
</h4>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Host/Name:
</label>
<div className="bg-white rounded-lg p-3 border border-blue-300 font-mono text-sm">
{siteData.domain}
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Type:
</label>
<div className="bg-white rounded-lg p-3 border border-blue-300 font-mono text-sm">
TXT
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">
Value:
</label>
<div className="flex gap-2">
<input
type="text"
value={`maplepress-verify=${siteData.verification_token}`}
readOnly
className="flex-1 px-4 py-3 border border-blue-300 rounded-lg bg-white font-mono text-sm"
/>
<button
onClick={handleCopyVerificationToken}
className={`px-6 py-3 rounded-lg font-medium transition-all
${copiedToken
? "bg-green-600 text-white"
: "bg-blue-600 text-white hover:bg-blue-700"
}`}
>
{copiedToken ? "✓ Copied!" : "Copy"}
</button>
</div>
</div>
</div>
<div className="mt-6 p-4 bg-blue-100 rounded-lg">
<h5 className="font-semibold text-gray-900 mb-2 text-sm">
📝 Instructions:
</h5>
<ol className="text-sm text-gray-700 space-y-1 list-decimal list-inside">
<li>Log in to your domain registrar (GoDaddy, Namecheap, Cloudflare, etc.)</li>
<li>Find DNS settings or DNS management</li>
<li>Add a new TXT record with the values above</li>
<li>Wait 5-10 minutes for DNS propagation</li>
<li>Click "Verify Domain" in the MaplePress WordPress plugin</li>
</ol>
<p className="text-xs text-gray-600 mt-3">
<strong>Note:</strong> DNS changes can take up to 48 hours to propagate globally, but usually complete within 10 minutes.
</p>
</div>
</div>
</div>
</div>
{/* Next Steps */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden mb-6">
<div className="p-8">
<h3 className="text-lg font-semibold text-gray-900 mb-4">Next Steps</h3>
<ol className="space-y-4">
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-8 h-8 bg-indigo-600 text-white rounded-full
flex items-center justify-center text-sm font-bold">
1
</span>
<div>
<h4 className="font-semibold text-gray-900">Add DNS TXT Record</h4>
<p className="text-sm text-gray-600 mt-1">
Add the DNS TXT record shown above to your domain registrar. This proves you own the domain.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-8 h-8 bg-indigo-600 text-white rounded-full
flex items-center justify-center text-sm font-bold">
2
</span>
<div>
<h4 className="font-semibold text-gray-900">Install the MaplePress WordPress Plugin</h4>
<p className="text-sm text-gray-600 mt-1">
Download and install the MaplePress plugin on your WordPress site.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-8 h-8 bg-indigo-600 text-white rounded-full
flex items-center justify-center text-sm font-bold">
3
</span>
<div>
<h4 className="font-semibold text-gray-900">Configure the Plugin with Your API Key</h4>
<p className="text-sm text-gray-600 mt-1">
Go to your WordPress admin panel Settings MaplePress and enter the API key you saved earlier.
</p>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-8 h-8 bg-indigo-600 text-white rounded-full
flex items-center justify-center text-sm font-bold">
4
</span>
<div>
<h4 className="font-semibold text-gray-900">Verify Your Domain</h4>
<p className="text-sm text-gray-600 mt-1">
After DNS propagation (5-10 minutes), click "Verify Domain" in the plugin to activate cloud-powered features.
</p>
</div>
</li>
</ol>
</div>
</div>
{/* Action Button */}
<div className="flex justify-center">
<button
onClick={handleDone}
className="px-8 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700
transition-all font-medium text-lg shadow-lg"
>
Go to Dashboard
</button>
</div>
</main>
</div>
);
}
export default AddSiteSuccess;

View file

@ -0,0 +1,404 @@
// File: src/pages/Sites/DeleteSite.jsx
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { useAuth } from "../../services/Services";
import SiteService from "../../services/API/SiteService";
/**
* DeleteSite - Confirmation page for deleting a WordPress site
*/
function DeleteSite() {
const navigate = useNavigate();
const { id } = useParams();
const { authManager } = useAuth();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [site, setSite] = useState(null);
const [siteLoading, setSiteLoading] = useState(false);
const [siteError, setSiteError] = useState(null);
const [isDeleting, setIsDeleting] = useState(false);
const [deleteError, setDeleteError] = useState(null);
const [confirmationText, setConfirmationText] = useState("");
useEffect(() => {
// Wait for AuthManager to initialize before checking authentication
const checkAuth = async () => {
const isInitialized = authManager.getIsInitialized();
if (!isInitialized) {
setTimeout(checkAuth, 50);
return;
}
const isAuth = authManager.isAuthenticated();
if (!isAuth) {
navigate("/login");
return;
}
// Proactively refresh token if needed
try {
await authManager.ensureValidToken();
} catch (error) {
console.error("[DeleteSite] Token refresh failed:", error);
navigate("/login");
return;
}
const userData = authManager.getUser();
setUser(userData);
setIsLoading(false);
};
checkAuth();
}, [authManager, navigate]);
// Background token refresh - check every 30 seconds
useEffect(() => {
const refreshInterval = setInterval(async () => {
if (!authManager.getIsInitialized()) {
return;
}
try {
await authManager.ensureValidToken();
} catch (error) {
console.error("[DeleteSite] Background token refresh failed:", error);
navigate("/login");
}
}, 30000);
return () => clearInterval(refreshInterval);
}, [authManager, navigate]);
// Load site details
useEffect(() => {
if (!user || isLoading || !id) {
return;
}
const loadSite = async () => {
setSiteLoading(true);
setSiteError(null);
console.log("[DeleteSite] Loading site:", id);
try {
const siteData = await SiteService.getSiteById(id);
console.log("[DeleteSite] Site loaded:", siteData);
setSite(siteData);
} catch (error) {
console.error("[DeleteSite] Failed to load site:", error);
setSiteError(error.message || "Failed to load site details");
} finally {
setSiteLoading(false);
}
};
loadSite();
}, [user, isLoading, id]);
const handleDeleteSite = async (e) => {
e.preventDefault();
// Validate confirmation text
if (confirmationText !== site.domain) {
setDeleteError(`Please type "${site.domain}" to confirm deletion`);
return;
}
setIsDeleting(true);
setDeleteError(null);
console.log("[DeleteSite] Deleting site:", id);
try {
await SiteService.deleteSite(id);
console.log("[DeleteSite] Site deleted successfully");
// Show success message and redirect to dashboard
navigate("/dashboard", {
state: {
message: `Site "${site.domain}" has been deleted successfully`,
type: "success"
}
});
} catch (error) {
console.error("[DeleteSite] Failed to delete site:", error);
setDeleteError(error.message || "Failed to delete site. Please try again.");
setIsDeleting(false);
}
};
const handleCancel = () => {
navigate(`/sites/${id}`);
};
// Show loading state while waiting for authentication check
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-600 border-t-transparent"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100">
{/* Navigation Bar */}
<nav className="bg-white border-b border-gray-200 shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<button
onClick={() => navigate("/dashboard")}
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
>
<span className="text-2xl">🍁</span>
<h1 className="text-xl font-bold bg-gradient-to-r from-indigo-600 to-blue-600 bg-clip-text text-transparent">
MaplePress
</h1>
</button>
{/* User Menu */}
<div className="flex items-center gap-2 px-4 py-2 bg-gray-50 rounded-lg">
<div className="w-8 h-8 bg-gradient-to-r from-indigo-600 to-blue-600 rounded-full
flex items-center justify-center text-white text-sm font-semibold">
{user?.email?.[0]?.toUpperCase() || "U"}
</div>
<span className="text-sm font-medium text-gray-700">
{user?.email || "User"}
</span>
</div>
</div>
</div>
</nav>
{/* Main Content */}
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Back Button */}
<button
onClick={handleCancel}
className="text-indigo-600 hover:text-indigo-700 font-medium mb-6 flex items-center gap-2"
>
<span></span>
<span>Back to Site Details</span>
</button>
{/* Loading State */}
{siteLoading && (
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-12 text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-600 border-t-transparent"></div>
<p className="mt-4 text-gray-600">Loading site details...</p>
</div>
)}
{/* Error State */}
{!siteLoading && siteError && (
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-12 text-center">
<div className="w-20 h-20 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
<span className="text-4xl"></span>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">
Failed to Load Site
</h2>
<p className="text-gray-600 mb-6">{siteError}</p>
<div className="flex gap-3 justify-center">
<button
onClick={() => window.location.reload()}
className="px-6 py-3 bg-gradient-to-r from-indigo-600 to-blue-600 text-white
rounded-lg hover:from-indigo-700 hover:to-blue-700 transition-all font-medium"
>
Retry
</button>
<button
onClick={() => navigate("/dashboard")}
className="px-6 py-3 text-gray-700 border border-gray-300 rounded-lg
hover:bg-gray-50 transition-all font-medium"
>
Back to Dashboard
</button>
</div>
</div>
)}
{/* Delete Confirmation Form */}
{!siteLoading && !siteError && site && (
<div className="bg-white rounded-2xl shadow-lg border border-red-200 overflow-hidden">
{/* Warning Header */}
<div className="bg-gradient-to-r from-red-600 to-red-700 px-6 py-8">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-white rounded-lg flex items-center justify-center text-4xl">
</div>
<div>
<h1 className="text-3xl font-bold text-white">
Delete Site
</h1>
<p className="text-red-100 mt-1">
This action cannot be undone
</p>
</div>
</div>
</div>
{/* Delete Form */}
<div className="p-8">
{/* Site Information */}
<div className="mb-8 p-6 bg-gray-50 rounded-lg border border-gray-200">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Site to be deleted:
</h2>
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-indigo-500 to-blue-500 rounded-lg
flex items-center justify-center text-white font-bold text-lg">
{site.domain[0]?.toUpperCase() || "W"}
</div>
<div>
<div className="font-semibold text-gray-900 flex items-center gap-2">
{site.domain}
{site.isVerified && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
Verified
</span>
)}
</div>
<div className="text-sm text-gray-600">{site.siteUrl}</div>
</div>
</div>
<div className="grid grid-cols-2 gap-4 mt-4 text-sm">
<div>
<span className="text-gray-600">Pages Indexed:</span>
<span className="ml-2 font-semibold text-gray-900">{site.totalPagesIndexed}</span>
</div>
<div>
<span className="text-gray-600">Search Requests:</span>
<span className="ml-2 font-semibold text-gray-900">{site.searchRequestsCount}</span>
</div>
<div>
<span className="text-gray-600">Storage Used:</span>
<span className="ml-2 font-semibold text-gray-900">
{SiteService.formatStorage(site.storageUsedBytes)}
</span>
</div>
<div>
<span className="text-gray-600">Created:</span>
<span className="ml-2 font-semibold text-gray-900">
{site.createdAt.toLocaleDateString()}
</span>
</div>
</div>
</div>
</div>
{/* Warning Box */}
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start gap-3">
<span className="text-2xl">🚨</span>
<div className="flex-1">
<h3 className="font-semibold text-red-900 mb-2">
Warning: This action is permanent
</h3>
<ul className="space-y-1 text-sm text-red-800">
<li> All indexed pages will be permanently deleted</li>
<li> Search index will be destroyed</li>
<li> All usage statistics will be lost</li>
<li> API key will be revoked immediately</li>
<li> This action cannot be undone</li>
</ul>
</div>
</div>
</div>
{/* Delete Error */}
{deleteError && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start gap-3">
<span className="text-xl"></span>
<div className="flex-1">
<p className="text-sm text-red-800">{deleteError}</p>
</div>
</div>
</div>
)}
{/* Confirmation Form */}
<form onSubmit={handleDeleteSite}>
<div className="mb-6">
<label
htmlFor="confirmationText"
className="block text-sm font-medium text-gray-700 mb-2"
>
Type <span className="font-mono font-bold text-red-600">{site.domain}</span> to confirm deletion:
</label>
<input
type="text"
id="confirmationText"
value={confirmationText}
onChange={(e) => {
setConfirmationText(e.target.value);
setDeleteError(null);
}}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-transparent"
placeholder={site.domain}
disabled={isDeleting}
autoFocus
/>
<p className="mt-1 text-sm text-gray-500">
Please type the domain name exactly as shown above to confirm
</p>
</div>
{/* Action Buttons */}
<div className="flex gap-3">
<button
type="submit"
disabled={isDeleting || confirmationText !== site.domain}
className="flex-1 px-6 py-3 bg-red-600 text-white rounded-lg hover:bg-red-700
transition-all font-medium shadow-lg hover:shadow-xl
disabled:opacity-50 disabled:cursor-not-allowed"
>
{isDeleting ? "Deleting Site..." : "Delete Site Permanently"}
</button>
<button
type="button"
onClick={handleCancel}
disabled={isDeleting}
className="px-6 py-3 text-gray-700 border border-gray-300 rounded-lg
hover:bg-gray-50 transition-all font-medium
disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancel
</button>
</div>
</form>
{/* Additional Information */}
<div className="mt-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-start gap-3">
<span className="text-xl">💡</span>
<div className="flex-1">
<h4 className="font-semibold text-blue-900 mb-1">
Need to reconnect later?
</h4>
<p className="text-sm text-blue-800">
If you delete this site and want to reconnect it later, you'll need to register
it again and configure a new API key in your WordPress plugin.
</p>
</div>
</div>
</div>
</div>
</div>
)}
</main>
</div>
);
}
export default DeleteSite;

View file

@ -0,0 +1,528 @@
// File: src/pages/Sites/RotateApiKey.jsx
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { useAuth } from "../../services/Services";
import SiteService from "../../services/API/SiteService";
/**
* RotateApiKey - Page for rotating a site's API key
*/
function RotateApiKey() {
const navigate = useNavigate();
const { id } = useParams();
const { authManager } = useAuth();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [site, setSite] = useState(null);
const [siteLoading, setSiteLoading] = useState(false);
const [siteError, setSiteError] = useState(null);
const [isRotating, setIsRotating] = useState(false);
const [rotateError, setRotateError] = useState(null);
const [rotationResult, setRotationResult] = useState(null);
const [confirmationText, setConfirmationText] = useState("");
const [apiKeyCopied, setApiKeyCopied] = useState(false);
useEffect(() => {
// Wait for AuthManager to initialize before checking authentication
const checkAuth = async () => {
const isInitialized = authManager.getIsInitialized();
if (!isInitialized) {
setTimeout(checkAuth, 50);
return;
}
const isAuth = authManager.isAuthenticated();
if (!isAuth) {
navigate("/login");
return;
}
// Proactively refresh token if needed
try {
await authManager.ensureValidToken();
} catch (error) {
console.error("[RotateApiKey] Token refresh failed:", error);
navigate("/login");
return;
}
const userData = authManager.getUser();
setUser(userData);
setIsLoading(false);
};
checkAuth();
}, [authManager, navigate]);
// Background token refresh - check every 30 seconds
useEffect(() => {
const refreshInterval = setInterval(async () => {
if (!authManager.getIsInitialized()) {
return;
}
try {
await authManager.ensureValidToken();
} catch (error) {
console.error("[RotateApiKey] Background token refresh failed:", error);
navigate("/login");
}
}, 30000);
return () => clearInterval(refreshInterval);
}, [authManager, navigate]);
// Load site details
useEffect(() => {
if (!user || isLoading || !id) {
return;
}
const loadSite = async () => {
setSiteLoading(true);
setSiteError(null);
console.log("[RotateApiKey] Loading site:", id);
try {
const siteData = await SiteService.getSiteById(id);
console.log("[RotateApiKey] Site loaded:", siteData);
setSite(siteData);
} catch (error) {
console.error("[RotateApiKey] Failed to load site:", error);
setSiteError(error.message || "Failed to load site details");
} finally {
setSiteLoading(false);
}
};
loadSite();
}, [user, isLoading, id]);
const handleRotateApiKey = async (e) => {
e.preventDefault();
// Validate confirmation text
if (confirmationText.toLowerCase() !== "rotate") {
setRotateError('Please type "ROTATE" to confirm');
return;
}
setIsRotating(true);
setRotateError(null);
console.log("[RotateApiKey] Rotating API key for site:", id);
try {
const result = await SiteService.rotateApiKey(id);
console.log("[RotateApiKey] API key rotated successfully");
setRotationResult(result);
} catch (error) {
console.error("[RotateApiKey] Failed to rotate API key:", error);
setRotateError(error.message || "Failed to rotate API key. Please try again.");
setIsRotating(false);
}
};
const handleCopyApiKey = () => {
if (rotationResult?.newApiKey) {
navigator.clipboard.writeText(rotationResult.newApiKey);
setApiKeyCopied(true);
setTimeout(() => setApiKeyCopied(false), 3000);
}
};
const handleDone = () => {
navigate(`/sites/${id}`);
};
const handleCancel = () => {
navigate(`/sites/${id}`);
};
// Show loading state while waiting for authentication check
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-600 border-t-transparent"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100">
{/* Navigation Bar */}
<nav className="bg-white border-b border-gray-200 shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<button
onClick={() => navigate("/dashboard")}
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
>
<span className="text-2xl">🍁</span>
<h1 className="text-xl font-bold bg-gradient-to-r from-indigo-600 to-blue-600 bg-clip-text text-transparent">
MaplePress
</h1>
</button>
{/* User Menu */}
<div className="flex items-center gap-2 px-4 py-2 bg-gray-50 rounded-lg">
<div className="w-8 h-8 bg-gradient-to-r from-indigo-600 to-blue-600 rounded-full
flex items-center justify-center text-white text-sm font-semibold">
{user?.email?.[0]?.toUpperCase() || "U"}
</div>
<span className="text-sm font-medium text-gray-700">
{user?.email || "User"}
</span>
</div>
</div>
</div>
</nav>
{/* Main Content */}
<main className="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Back Button */}
<button
onClick={handleCancel}
className="text-indigo-600 hover:text-indigo-700 font-medium mb-6 flex items-center gap-2"
>
<span></span>
<span>Back to Site Details</span>
</button>
{/* Loading State */}
{siteLoading && (
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-12 text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-600 border-t-transparent"></div>
<p className="mt-4 text-gray-600">Loading site details...</p>
</div>
)}
{/* Error State */}
{!siteLoading && siteError && (
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-12 text-center">
<div className="w-20 h-20 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
<span className="text-4xl"></span>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">
Failed to Load Site
</h2>
<p className="text-gray-600 mb-6">{siteError}</p>
<div className="flex gap-3 justify-center">
<button
onClick={() => window.location.reload()}
className="px-6 py-3 bg-gradient-to-r from-indigo-600 to-blue-600 text-white
rounded-lg hover:from-indigo-700 hover:to-blue-700 transition-all font-medium"
>
Retry
</button>
<button
onClick={() => navigate("/dashboard")}
className="px-6 py-3 text-gray-700 border border-gray-300 rounded-lg
hover:bg-gray-50 transition-all font-medium"
>
Back to Dashboard
</button>
</div>
</div>
)}
{/* Success State - Show New API Key */}
{!siteLoading && !siteError && site && rotationResult && (
<div className="bg-white rounded-2xl shadow-lg border border-green-200 overflow-hidden">
{/* Success Header */}
<div className="bg-gradient-to-r from-green-600 to-emerald-600 px-6 py-8">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-white rounded-lg flex items-center justify-center text-4xl">
</div>
<div>
<h1 className="text-3xl font-bold text-white">
API Key Rotated Successfully
</h1>
<p className="text-green-100 mt-1">
Your new API key has been generated
</p>
</div>
</div>
</div>
{/* New API Key Display */}
<div className="p-8">
<div className="mb-6 p-6 bg-yellow-50 border-2 border-yellow-400 rounded-lg">
<div className="flex items-start gap-3 mb-4">
<span className="text-3xl"></span>
<div className="flex-1">
<h3 className="font-bold text-yellow-900 text-lg mb-2">
Save Your New API Key Now!
</h3>
<p className="text-sm text-yellow-800 mb-2">
This is the <strong>only time</strong> you'll be able to see this API key.
Copy it now and update your WordPress plugin settings immediately.
</p>
</div>
</div>
{/* API Key Display with Copy Button */}
<div className="bg-white border border-yellow-300 rounded-lg p-4">
<label className="block text-sm font-medium text-gray-700 mb-2">
New API Key:
</label>
<div className="flex gap-2">
<input
type="text"
value={rotationResult.newApiKey}
readOnly
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg bg-gray-50 font-mono text-sm"
/>
<button
onClick={handleCopyApiKey}
className="px-6 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700
transition-all font-medium whitespace-nowrap"
>
{apiKeyCopied ? "Copied!" : "Copy"}
</button>
</div>
</div>
</div>
{/* Immediate Action Required */}
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start gap-3">
<span className="text-2xl">🚨</span>
<div className="flex-1">
<h4 className="font-semibold text-red-900 mb-2">
Old API Key Immediately Invalidated
</h4>
<p className="text-sm text-red-800 mb-2">
Your old API key (ending in <span className="font-mono font-bold">{rotationResult.oldKeyLastFour}</span>)
has been <strong>immediately deactivated</strong> and will no longer work.
</p>
<p className="text-sm text-red-800 mt-2">
You must update your WordPress plugin with the new API key <strong>right now</strong> to restore functionality.
</p>
</div>
</div>
</div>
{/* Site Information */}
<div className="mb-6 p-4 bg-gray-50 border border-gray-200 rounded-lg">
<h4 className="font-semibold text-gray-900 mb-3">Site Details:</h4>
<div className="space-y-2 text-sm">
<div className="flex items-center gap-2">
<span className="text-gray-600">Domain:</span>
<span className="font-semibold text-gray-900">{site.domain}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-gray-600">Site URL:</span>
<span className="font-semibold text-gray-900">{site.siteUrl}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-gray-600">Rotated At:</span>
<span className="font-semibold text-gray-900">{rotationResult.rotatedAt.toLocaleString()}</span>
</div>
</div>
</div>
{/* Next Steps */}
<div className="mb-6 p-4 bg-indigo-50 border border-indigo-200 rounded-lg">
<div className="flex items-start gap-3">
<span className="text-2xl">📝</span>
<div className="flex-1">
<h4 className="font-semibold text-indigo-900 mb-2">
Next Steps
</h4>
<ol className="space-y-2 text-sm text-indigo-800">
<li>1. Copy the new API key using the button above</li>
<li>2. Log in to your WordPress admin panel</li>
<li>3. Go to Settings MaplePress</li>
<li>4. Paste the new API key in the settings</li>
<li>5. Save the settings to activate the new key</li>
</ol>
</div>
</div>
</div>
{/* Done Button */}
<div className="flex gap-3">
<button
onClick={handleDone}
className="flex-1 px-6 py-3 bg-gradient-to-r from-indigo-600 to-blue-600 text-white
rounded-lg hover:from-indigo-700 hover:to-blue-700 transition-all
font-medium shadow-lg hover:shadow-xl"
>
Done - Return to Site Details
</button>
</div>
</div>
</div>
)}
{/* Rotation Confirmation Form */}
{!siteLoading && !siteError && site && !rotationResult && (
<div className="bg-white rounded-2xl shadow-lg border border-yellow-200 overflow-hidden">
{/* Warning Header */}
<div className="bg-gradient-to-r from-yellow-600 to-amber-600 px-6 py-8">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-white rounded-lg flex items-center justify-center text-4xl">
🔑
</div>
<div>
<h1 className="text-3xl font-bold text-white">
Rotate API Key
</h1>
<p className="text-yellow-100 mt-1">
Generate a new API key for your site
</p>
</div>
</div>
</div>
{/* Rotation Form */}
<div className="p-8">
{/* Site Information */}
<div className="mb-6 p-6 bg-gray-50 rounded-lg border border-gray-200">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Site Information:
</h2>
<div className="space-y-3">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-indigo-500 to-blue-500 rounded-lg
flex items-center justify-center text-white font-bold text-lg">
{site.domain[0]?.toUpperCase() || "W"}
</div>
<div>
<div className="font-semibold text-gray-900 flex items-center gap-2">
{site.domain}
{site.isVerified && (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
Verified
</span>
)}
</div>
<div className="text-sm text-gray-600">{site.siteUrl}</div>
</div>
</div>
<div className="mt-4 text-sm">
<span className="text-gray-600">Current API Key:</span>
<span className="ml-2 font-mono font-semibold text-gray-900">
{site.apiKeyPrefix}{site.apiKeyLastFour}
</span>
</div>
</div>
</div>
{/* Warning Box */}
<div className="mb-6 p-4 bg-red-50 border border-red-300 rounded-lg">
<div className="flex items-start gap-3">
<span className="text-2xl">🚨</span>
<div className="flex-1">
<h3 className="font-semibold text-red-900 mb-2">
Critical: What Happens When You Rotate
</h3>
<ul className="space-y-1 text-sm text-red-800">
<li> A new API key will be generated <strong>immediately</strong></li>
<li> The old API key will be <strong>invalidated instantly</strong> - no grace period!</li>
<li> Your WordPress site functionality will stop working until you update the key</li>
<li> You must update your WordPress plugin settings with the new key right away</li>
<li> The new key will be shown only once - save it immediately!</li>
</ul>
</div>
</div>
</div>
{/* When to Rotate */}
<div className="mb-6 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<div className="flex items-start gap-3">
<span className="text-2xl">💡</span>
<div className="flex-1">
<h4 className="font-semibold text-blue-900 mb-2">
When Should You Rotate Your API Key?
</h4>
<ul className="space-y-1 text-sm text-blue-800">
<li> Your API key has been compromised or exposed</li>
<li> You suspect unauthorized access to your site</li>
<li> As part of regular security maintenance</li>
<li> When removing access for a third party</li>
</ul>
</div>
</div>
</div>
{/* Rotation Error */}
{rotateError && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
<div className="flex items-start gap-3">
<span className="text-xl"></span>
<div className="flex-1">
<p className="text-sm text-red-800">{rotateError}</p>
</div>
</div>
</div>
)}
{/* Confirmation Form */}
<form onSubmit={handleRotateApiKey}>
<div className="mb-6">
<label
htmlFor="confirmationText"
className="block text-sm font-medium text-gray-700 mb-2"
>
Type <span className="font-mono font-bold text-yellow-600">ROTATE</span> to confirm:
</label>
<input
type="text"
id="confirmationText"
value={confirmationText}
onChange={(e) => {
setConfirmationText(e.target.value);
setRotateError(null);
}}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-yellow-500 focus:border-transparent"
placeholder="ROTATE"
disabled={isRotating}
autoFocus
/>
<p className="mt-1 text-sm text-gray-500">
Type "ROTATE" in capital letters to confirm the rotation
</p>
</div>
{/* Action Buttons */}
<div className="flex gap-3">
<button
type="submit"
disabled={isRotating || confirmationText.toLowerCase() !== "rotate"}
className="flex-1 px-6 py-3 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700
transition-all font-medium shadow-lg hover:shadow-xl
disabled:opacity-50 disabled:cursor-not-allowed"
>
{isRotating ? "Rotating API Key..." : "Rotate API Key"}
</button>
<button
type="button"
onClick={handleCancel}
disabled={isRotating}
className="px-6 py-3 text-gray-700 border border-gray-300 rounded-lg
hover:bg-gray-50 transition-all font-medium
disabled:opacity-50 disabled:cursor-not-allowed"
>
Cancel
</button>
</div>
</form>
</div>
</div>
)}
</main>
</div>
);
}
export default RotateApiKey;

View file

@ -0,0 +1,348 @@
// File: src/pages/Sites/SiteDetail.jsx
import React, { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router";
import { useAuth } from "../../services/Services";
import SiteService from "../../services/API/SiteService";
/**
* SiteDetail - Detailed view of a WordPress site with management options
*/
function SiteDetail() {
const navigate = useNavigate();
const { id } = useParams();
const { authManager } = useAuth();
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(true);
const [site, setSite] = useState(null);
const [siteLoading, setSiteLoading] = useState(false);
const [siteError, setSiteError] = useState(null);
useEffect(() => {
// Wait for AuthManager to initialize before checking authentication
const checkAuth = async () => {
const isInitialized = authManager.getIsInitialized();
if (!isInitialized) {
setTimeout(checkAuth, 50);
return;
}
const isAuth = authManager.isAuthenticated();
if (!isAuth) {
navigate("/login");
return;
}
// Proactively refresh token if needed
try {
await authManager.ensureValidToken();
} catch (error) {
console.error("[SiteDetail] Token refresh failed:", error);
navigate("/login");
return;
}
const userData = authManager.getUser();
setUser(userData);
setIsLoading(false);
};
checkAuth();
}, [authManager, navigate]);
// Background token refresh - check every 30 seconds
useEffect(() => {
const refreshInterval = setInterval(async () => {
if (!authManager.getIsInitialized()) {
return;
}
try {
await authManager.ensureValidToken();
} catch (error) {
console.error("[SiteDetail] Background token refresh failed:", error);
navigate("/login");
}
}, 30000);
return () => clearInterval(refreshInterval);
}, [authManager, navigate]);
// Load site details
useEffect(() => {
if (!user || isLoading || !id) {
return;
}
const loadSite = async () => {
setSiteLoading(true);
setSiteError(null);
console.log("[SiteDetail] Loading site:", id);
try {
const siteData = await SiteService.getSiteById(id);
console.log("[SiteDetail] Site loaded:", siteData);
setSite(siteData);
} catch (error) {
console.error("[SiteDetail] Failed to load site:", error);
setSiteError(error.message || "Failed to load site details");
} finally {
setSiteLoading(false);
}
};
loadSite();
}, [user, isLoading, id]);
const handleNavigateToDelete = () => {
navigate(`/sites/${id}/delete`);
};
const handleNavigateToRotateKey = () => {
navigate(`/sites/${id}/rotate-key`);
};
// Show loading state while waiting for authentication check
if (isLoading) {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 flex items-center justify-center">
<div className="text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-600 border-t-transparent"></div>
<p className="mt-4 text-gray-600">Loading...</p>
</div>
</div>
);
}
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100">
{/* Navigation Bar */}
<nav className="bg-white border-b border-gray-200 shadow-sm sticky top-0 z-50">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center h-16">
{/* Logo */}
<button
onClick={() => navigate("/dashboard")}
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
>
<span className="text-2xl">🍁</span>
<h1 className="text-xl font-bold bg-gradient-to-r from-indigo-600 to-blue-600 bg-clip-text text-transparent">
MaplePress
</h1>
</button>
{/* User Menu */}
<div className="flex items-center gap-2 px-4 py-2 bg-gray-50 rounded-lg">
<div className="w-8 h-8 bg-gradient-to-r from-indigo-600 to-blue-600 rounded-full
flex items-center justify-center text-white text-sm font-semibold">
{user?.email?.[0]?.toUpperCase() || "U"}
</div>
<span className="text-sm font-medium text-gray-700">
{user?.email || "User"}
</span>
</div>
</div>
</div>
</nav>
{/* Main Content */}
<main className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Back Button */}
<button
onClick={() => navigate("/dashboard")}
className="text-indigo-600 hover:text-indigo-700 font-medium mb-6 flex items-center gap-2"
>
<span></span>
<span>Back to Dashboard</span>
</button>
{/* Loading State */}
{siteLoading && (
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-12 text-center">
<div className="inline-block animate-spin rounded-full h-12 w-12 border-4 border-indigo-600 border-t-transparent"></div>
<p className="mt-4 text-gray-600">Loading site details...</p>
</div>
)}
{/* Error State */}
{!siteLoading && siteError && (
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 p-12 text-center">
<div className="w-20 h-20 bg-red-100 rounded-full flex items-center justify-center mx-auto mb-6">
<span className="text-4xl"></span>
</div>
<h2 className="text-2xl font-bold text-gray-900 mb-3">
Failed to Load Site
</h2>
<p className="text-gray-600 mb-6">{siteError}</p>
<div className="flex gap-3 justify-center">
<button
onClick={() => window.location.reload()}
className="px-6 py-3 bg-gradient-to-r from-indigo-600 to-blue-600 text-white
rounded-lg hover:from-indigo-700 hover:to-blue-700 transition-all font-medium"
>
Retry
</button>
<button
onClick={() => navigate("/dashboard")}
className="px-6 py-3 text-gray-700 border border-gray-300 rounded-lg
hover:bg-gray-50 transition-all font-medium"
>
Back to Dashboard
</button>
</div>
</div>
)}
{/* Site Details */}
{!siteLoading && !siteError && site && (
<div className="space-y-6">
{/* Site Header */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
<div className="bg-gradient-to-r from-indigo-600 to-blue-600 px-6 py-8">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-white rounded-lg flex items-center justify-center text-indigo-600 font-bold text-2xl">
{site.domain[0]?.toUpperCase() || "W"}
</div>
<div>
<h1 className="text-3xl font-bold text-white flex items-center gap-3">
{site.domain}
{site.isVerified && (
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-400 text-green-900">
Verified
</span>
)}
{!site.isVerified && (
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-400 text-yellow-900">
Pending Verification
</span>
)}
</h1>
<p className="text-indigo-100 mt-1">{site.siteUrl}</p>
</div>
</div>
</div>
</div>
{/* Site Information */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900">Site Information</h2>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">Site ID</label>
<p className="text-gray-900 font-mono text-sm">{site.id}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">Status</label>
<p className="text-gray-900">{site.status}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">API Key</label>
<p className="text-gray-900 font-mono text-sm">
{site.apiKeyPrefix}{site.apiKeyLastFour}
</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">Search Index</label>
<p className="text-gray-900 font-mono text-sm">{site.searchIndexName}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">Created</label>
<p className="text-gray-900">{site.createdAt.toLocaleString()}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-500 mb-1">Last Updated</label>
<p className="text-gray-900">{site.updatedAt.toLocaleString()}</p>
</div>
</div>
</div>
</div>
{/* Usage Statistics */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900">Usage Statistics</h2>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center p-4 bg-gradient-to-br from-indigo-50 to-blue-50 rounded-lg">
<div className="text-3xl font-bold text-indigo-600">{site.totalPagesIndexed}</div>
<div className="text-sm text-gray-600 mt-1">Total Pages Indexed</div>
</div>
<div className="text-center p-4 bg-gradient-to-br from-green-50 to-emerald-50 rounded-lg">
<div className="text-3xl font-bold text-green-600">{site.searchRequestsCount}</div>
<div className="text-sm text-gray-600 mt-1">Search Requests</div>
</div>
<div className="text-center p-4 bg-gradient-to-br from-purple-50 to-pink-50 rounded-lg">
<div className="text-3xl font-bold text-purple-600">
{SiteService.formatStorage(site.storageUsedBytes)}
</div>
<div className="text-sm text-gray-600 mt-1">Storage Used</div>
</div>
</div>
{site.lastIndexedAt && (
<div className="mt-4 text-center text-sm text-gray-600">
Last indexed: {site.lastIndexedAt.toLocaleString()}
</div>
)}
{site.pluginVersion && (
<div className="mt-2 text-center text-sm text-gray-600">
Plugin version: {site.pluginVersion}
</div>
)}
</div>
</div>
{/* Actions */}
<div className="bg-white rounded-2xl shadow-lg border border-gray-100 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-xl font-bold text-gray-900">Site Management</h2>
</div>
<div className="p-6 space-y-4">
{/* Rotate API Key */}
<div className="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
<div>
<h3 className="font-semibold text-gray-900">Rotate API Key</h3>
<p className="text-sm text-gray-600">
Generate a new API key if the current one is compromised
</p>
</div>
<button
onClick={handleNavigateToRotateKey}
className="px-4 py-2 bg-yellow-600 text-white rounded-lg hover:bg-yellow-700
transition-all font-medium"
>
Rotate Key
</button>
</div>
{/* Delete Site */}
<div className="flex items-center justify-between p-4 border border-red-200 rounded-lg bg-red-50">
<div>
<h3 className="font-semibold text-gray-900">Delete Site</h3>
<p className="text-sm text-gray-600">
Permanently delete this site and all associated data
</p>
</div>
<button
onClick={handleNavigateToDelete}
className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700
transition-all font-medium"
>
Delete Site
</button>
</div>
</div>
</div>
</div>
)}
</main>
</div>
);
}
export default SiteDetail;