Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
183
web/maplepress-frontend/src/pages/Auth/Login.jsx
Normal file
183
web/maplepress-frontend/src/pages/Auth/Login.jsx
Normal 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;
|
||||
529
web/maplepress-frontend/src/pages/Auth/Register.jsx
Normal file
529
web/maplepress-frontend/src/pages/Auth/Register.jsx
Normal 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;
|
||||
368
web/maplepress-frontend/src/pages/Dashboard/Dashboard.jsx
Normal file
368
web/maplepress-frontend/src/pages/Dashboard/Dashboard.jsx
Normal 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;
|
||||
279
web/maplepress-frontend/src/pages/Home/IndexPage.jsx
Normal file
279
web/maplepress-frontend/src/pages/Home/IndexPage.jsx
Normal 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;
|
||||
383
web/maplepress-frontend/src/pages/Sites/AddSite.jsx
Normal file
383
web/maplepress-frontend/src/pages/Sites/AddSite.jsx
Normal 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;
|
||||
543
web/maplepress-frontend/src/pages/Sites/AddSite.jsx.bak
Normal file
543
web/maplepress-frontend/src/pages/Sites/AddSite.jsx.bak
Normal 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;
|
||||
336
web/maplepress-frontend/src/pages/Sites/AddSiteSuccess.jsx
Normal file
336
web/maplepress-frontend/src/pages/Sites/AddSiteSuccess.jsx
Normal 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;
|
||||
404
web/maplepress-frontend/src/pages/Sites/DeleteSite.jsx
Normal file
404
web/maplepress-frontend/src/pages/Sites/DeleteSite.jsx
Normal 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;
|
||||
528
web/maplepress-frontend/src/pages/Sites/RotateApiKey.jsx
Normal file
528
web/maplepress-frontend/src/pages/Sites/RotateApiKey.jsx
Normal 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;
|
||||
348
web/maplepress-frontend/src/pages/Sites/SiteDetail.jsx
Normal file
348
web/maplepress-frontend/src/pages/Sites/SiteDetail.jsx
Normal 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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue