cleanup - froentend pages removed
This commit is contained in:
@@ -1,966 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import ComponentCard from "../../components/common/ComponentCard";
|
||||
import { API_BASE_URL, fetchContentImages, fetchUsageLimits, fetchAPI } from "../../services/api";
|
||||
|
||||
interface EndpointStatus {
|
||||
endpoint: string;
|
||||
method: string;
|
||||
status: 'healthy' | 'warning' | 'error' | 'checking';
|
||||
responseTime?: number;
|
||||
lastChecked?: string;
|
||||
error?: string;
|
||||
apiStatus?: 'healthy' | 'warning' | 'error'; // API endpoint status
|
||||
dataStatus?: 'healthy' | 'warning' | 'error'; // Page data population status
|
||||
}
|
||||
|
||||
interface EndpointGroup {
|
||||
name: string;
|
||||
endpoints: {
|
||||
path: string;
|
||||
method: string;
|
||||
description: string;
|
||||
pageFetchFunction?: () => Promise<any>; // Optional: function to test page data population
|
||||
dataValidator?: (data: any) => boolean; // Optional: function to validate data is populated
|
||||
}[];
|
||||
}
|
||||
|
||||
const endpointGroups: EndpointGroup[] = [
|
||||
{
|
||||
name: "Core Health & Auth",
|
||||
endpoints: [
|
||||
{ path: "/v1/system/status/", method: "GET", description: "System status" },
|
||||
{ path: "/v1/auth/login/", method: "POST", description: "Login" },
|
||||
{ path: "/v1/auth/me/", method: "GET", description: "Current user" },
|
||||
{ path: "/v1/auth/register/", method: "POST", description: "Registration" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Auth & User Management",
|
||||
endpoints: [
|
||||
{ path: "/v1/auth/users/", method: "GET", description: "List users" },
|
||||
{ path: "/v1/auth/accounts/", method: "GET", description: "List accounts" },
|
||||
{ path: "/v1/auth/sites/", method: "GET", description: "List sites" },
|
||||
{ path: "/v1/auth/sectors/", method: "GET", description: "List sectors" },
|
||||
{ path: "/v1/auth/plans/", method: "GET", description: "List plans" },
|
||||
{ path: "/v1/auth/industries/", method: "GET", description: "List industries" },
|
||||
{ path: "/v1/auth/seed-keywords/", method: "GET", description: "Seed keywords" },
|
||||
{ path: "/v1/auth/site-access/", method: "GET", description: "Site access" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Planner Module",
|
||||
endpoints: [
|
||||
{ path: "/v1/planner/keywords/", method: "GET", description: "List keywords" },
|
||||
{ path: "/v1/planner/keywords/auto_cluster/", method: "POST", description: "AI clustering" },
|
||||
{ path: "/v1/planner/keywords/bulk_delete/", method: "POST", description: "Bulk delete" },
|
||||
{ path: "/v1/planner/clusters/", method: "GET", description: "List clusters" },
|
||||
{ path: "/v1/planner/clusters/auto_generate_ideas/", method: "POST", description: "AI ideas" },
|
||||
{ path: "/v1/planner/ideas/", method: "GET", description: "List ideas" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Writer Module",
|
||||
endpoints: [
|
||||
{ path: "/v1/writer/tasks/", method: "GET", description: "List tasks" },
|
||||
{ path: "/v1/writer/tasks/auto_generate_content/", method: "POST", description: "AI content" },
|
||||
{ path: "/v1/writer/tasks/bulk_update/", method: "POST", description: "Bulk update" },
|
||||
{ path: "/v1/writer/content/", method: "GET", description: "List content" },
|
||||
{ path: "/v1/writer/content/generate_image_prompts/", method: "POST", description: "Image prompts" },
|
||||
{ path: "/v1/writer/images/", method: "GET", description: "List images" },
|
||||
{
|
||||
path: "/v1/writer/images/content_images/",
|
||||
method: "GET",
|
||||
description: "Content images",
|
||||
pageFetchFunction: async () => {
|
||||
const data = await fetchContentImages({});
|
||||
return data;
|
||||
},
|
||||
dataValidator: (data: any) => {
|
||||
// Check if data has results array with content
|
||||
return data && data.results && Array.isArray(data.results) && data.results.length > 0;
|
||||
}
|
||||
},
|
||||
{ path: "/v1/writer/images/generate_images/", method: "POST", description: "AI images" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "System & Billing",
|
||||
endpoints: [
|
||||
{ path: "/v1/system/prompts/", method: "GET", description: "List prompts" },
|
||||
{
|
||||
path: "/v1/system/prompts/by_type/clustering/",
|
||||
method: "GET",
|
||||
description: "Get prompt by type",
|
||||
pageFetchFunction: async () => {
|
||||
const response = await fetchAPI('/v1/system/prompts/by_type/clustering/');
|
||||
const data = response?.data || response;
|
||||
return data;
|
||||
},
|
||||
dataValidator: (data: any) => {
|
||||
// Check if prompt data exists and has prompt_value
|
||||
return data && data.prompt_type && (data.prompt_value !== null && data.prompt_value !== undefined);
|
||||
}
|
||||
},
|
||||
{ path: "/v1/system/prompts/save/", method: "POST", description: "Save prompt" },
|
||||
{ path: "/v1/system/author-profiles/", method: "GET", description: "List author profiles" },
|
||||
{ path: "/v1/system/strategies/", method: "GET", description: "List strategies" },
|
||||
{ path: "/v1/system/settings/integrations/openai/test/", method: "POST", description: "Test integration (OpenAI)" },
|
||||
{ path: "/v1/system/settings/account/", method: "GET", description: "Account settings" },
|
||||
{ path: "/v1/billing/credits/balance/", method: "GET", description: "Credit balance" },
|
||||
{ path: "/v1/billing/credits/usage/", method: "GET", description: "Usage logs" },
|
||||
{ path: "/v1/billing/credits/transactions/", method: "GET", description: "Transactions" },
|
||||
{ path: "/v1/billing/credits/usage/summary/", method: "GET", description: "Usage summary" },
|
||||
{
|
||||
path: "/v1/billing/credits/usage/limits/",
|
||||
method: "GET",
|
||||
description: "Usage limits",
|
||||
pageFetchFunction: async () => {
|
||||
const data = await fetchUsageLimits();
|
||||
return data;
|
||||
},
|
||||
dataValidator: (data: any) => {
|
||||
// Check if limits array exists and has content
|
||||
return data && data.limits && Array.isArray(data.limits) && data.limits.length > 0;
|
||||
}
|
||||
},
|
||||
{ path: "/v1/billing/credits/transactions/", method: "GET", description: "Transactions" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Billing (Customer)",
|
||||
endpoints: [
|
||||
{ path: "/v1/billing/credit-packages/", method: "GET", description: "List credit packages" },
|
||||
{ path: "/v1/billing/invoices/", method: "GET", description: "List invoices" },
|
||||
{ path: "/v1/billing/payments/", method: "GET", description: "List payments" },
|
||||
{ path: "/v1/billing/payment-methods/", method: "GET", description: "List payment methods" },
|
||||
{ path: "/v1/billing/payment-methods/available/", method: "GET", description: "Available payment methods" },
|
||||
{ path: "/v1/billing/payments/manual/", method: "POST", description: "Submit manual payment" },
|
||||
{ path: "/v1/billing/payments/available_methods/", method: "GET", description: "Payment methods (available_methods)" },
|
||||
{ path: "/v1/billing/payment-methods/1/set_default/", method: "POST", description: "Set default payment method (sample id)" },
|
||||
{ path: "/v1/billing/payment-methods/1/", method: "PATCH", description: "Update payment method (sample id)" },
|
||||
{ path: "/v1/billing/payment-methods/1/", method: "DELETE", description: "Delete payment method (sample id)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Admin Billing",
|
||||
endpoints: [
|
||||
{ path: "/v1/admin/billing/stats/", method: "GET", description: "Admin billing stats" },
|
||||
{ path: "/v1/admin/billing/invoices/", method: "GET", description: "Admin invoices" },
|
||||
{ path: "/v1/admin/billing/payments/", method: "GET", description: "Admin payments" },
|
||||
{ path: "/v1/admin/billing/pending_payments/", method: "GET", description: "Pending manual payments" },
|
||||
{ path: "/v1/admin/billing/1/approve_payment/", method: "POST", description: "Approve manual payment (sample id)" },
|
||||
{ path: "/v1/admin/billing/1/reject_payment/", method: "POST", description: "Reject manual payment (sample id)" },
|
||||
{ path: "/v1/admin/credit-costs/", method: "GET", description: "Credit cost configs" },
|
||||
{ path: "/v1/admin/credit-costs/", method: "POST", description: "Update credit cost configs" },
|
||||
{ path: "/v1/admin/users/", method: "GET", description: "Admin users with credits" },
|
||||
{ path: "/v1/admin/users/1/adjust-credits/", method: "POST", description: "Adjust credits (sample id)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "CRUD Operations - Planner",
|
||||
endpoints: [
|
||||
{ path: "/v1/planner/keywords/", method: "GET", description: "List keywords (READ)" },
|
||||
{ path: "/v1/planner/keywords/", method: "POST", description: "Create keyword (CREATE)" },
|
||||
{ path: "/v1/planner/keywords/1/", method: "GET", description: "Get keyword (READ)" },
|
||||
{ path: "/v1/planner/keywords/1/", method: "PUT", description: "Update keyword (UPDATE)" },
|
||||
{ path: "/v1/planner/keywords/1/", method: "DELETE", description: "Delete keyword (DELETE)" },
|
||||
{ path: "/v1/planner/clusters/", method: "GET", description: "List clusters (READ)" },
|
||||
{ path: "/v1/planner/clusters/", method: "POST", description: "Create cluster (CREATE)" },
|
||||
{ path: "/v1/planner/clusters/1/", method: "GET", description: "Get cluster (READ)" },
|
||||
{ path: "/v1/planner/clusters/1/", method: "PUT", description: "Update cluster (UPDATE)" },
|
||||
{ path: "/v1/planner/clusters/1/", method: "DELETE", description: "Delete cluster (DELETE)" },
|
||||
{ path: "/v1/planner/ideas/", method: "GET", description: "List ideas (READ)" },
|
||||
{ path: "/v1/planner/ideas/", method: "POST", description: "Create idea (CREATE)" },
|
||||
{ path: "/v1/planner/ideas/1/", method: "GET", description: "Get idea (READ)" },
|
||||
{ path: "/v1/planner/ideas/1/", method: "PUT", description: "Update idea (UPDATE)" },
|
||||
{ path: "/v1/planner/ideas/1/", method: "DELETE", description: "Delete idea (DELETE)" },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "CRUD Operations - Writer",
|
||||
endpoints: [
|
||||
{ path: "/v1/writer/tasks/", method: "GET", description: "List tasks (READ)" },
|
||||
{ path: "/v1/writer/tasks/", method: "POST", description: "Create task (CREATE)" },
|
||||
{ path: "/v1/writer/tasks/1/", method: "GET", description: "Get task (READ)" },
|
||||
{ path: "/v1/writer/tasks/1/", method: "PUT", description: "Update task (UPDATE)" },
|
||||
{ path: "/v1/writer/tasks/1/", method: "DELETE", description: "Delete task (DELETE)" },
|
||||
{ path: "/v1/writer/content/", method: "GET", description: "List content (READ)" },
|
||||
{ path: "/v1/writer/content/", method: "POST", description: "Create content (CREATE)" },
|
||||
{ path: "/v1/writer/content/1/", method: "GET", description: "Get content (READ)" },
|
||||
{ path: "/v1/writer/content/1/", method: "PUT", description: "Update content (UPDATE)" },
|
||||
{ path: "/v1/writer/content/1/", method: "DELETE", description: "Delete content (DELETE)" },
|
||||
{ path: "/v1/writer/images/", method: "GET", description: "List images (READ)" },
|
||||
{ path: "/v1/writer/images/", method: "POST", description: "Create image (CREATE)" },
|
||||
{ path: "/v1/writer/images/1/", method: "GET", description: "Get image (READ)" },
|
||||
{ path: "/v1/writer/images/1/", method: "PUT", description: "Update image (UPDATE)" },
|
||||
{ path: "/v1/writer/images/1/", method: "DELETE", description: "Delete image (DELETE)" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy': return 'text-green-600 dark:text-green-400';
|
||||
case 'warning': return 'text-yellow-600 dark:text-yellow-400';
|
||||
case 'error': return 'text-red-600 dark:text-red-400';
|
||||
case 'checking': return 'text-blue-600 dark:text-blue-400';
|
||||
default: return 'text-gray-600 dark:text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400';
|
||||
case 'warning': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400';
|
||||
case 'error': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
|
||||
case 'checking': return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400';
|
||||
default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy': return '✓';
|
||||
case 'warning': return '⚠';
|
||||
case 'error': return '✗';
|
||||
case 'checking': return '⟳';
|
||||
default: return '?';
|
||||
}
|
||||
};
|
||||
|
||||
export default function ApiMonitor() {
|
||||
const [endpointStatuses, setEndpointStatuses] = useState<Record<string, EndpointStatus>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [autoRefresh, setAutoRefresh] = useState(() => {
|
||||
// Load from localStorage
|
||||
const saved = localStorage.getItem('api-monitor-auto-refresh');
|
||||
return saved !== null ? saved === 'true' : true;
|
||||
});
|
||||
const [refreshInterval, setRefreshInterval] = useState(() => {
|
||||
// Load from localStorage, default to 30 seconds
|
||||
const saved = localStorage.getItem('api-monitor-refresh-interval');
|
||||
return saved ? parseInt(saved, 10) : 30;
|
||||
});
|
||||
|
||||
const checkEndpoint = useCallback(async (path: string, method: string, endpointConfig?: { pageFetchFunction?: () => Promise<any>; dataValidator?: (data: any) => boolean }) => {
|
||||
const key = `${method}:${path}`;
|
||||
|
||||
// Set checking status
|
||||
setEndpointStatuses(prev => ({
|
||||
...prev,
|
||||
[key]: {
|
||||
endpoint: path,
|
||||
method,
|
||||
status: 'checking',
|
||||
},
|
||||
}));
|
||||
|
||||
const startTime = Date.now();
|
||||
let apiStatus: 'healthy' | 'warning' | 'error' = 'healthy';
|
||||
let dataStatus: 'healthy' | 'warning' | 'error' = 'healthy';
|
||||
|
||||
try {
|
||||
// Get token from auth store or localStorage
|
||||
const token = localStorage.getItem('auth_token') ||
|
||||
(() => {
|
||||
try {
|
||||
const authStorage = localStorage.getItem('auth-storage');
|
||||
if (authStorage) {
|
||||
const parsed = JSON.parse(authStorage);
|
||||
return parsed?.state?.token || '';
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
return '';
|
||||
})();
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// Determine if this is an expensive AI endpoint that should use OPTIONS
|
||||
const isExpensiveAIEndpoint =
|
||||
path.includes('/auto_generate_content') ||
|
||||
path.includes('/auto_cluster') ||
|
||||
path.includes('/auto_generate_ideas') ||
|
||||
path.includes('/generate_image_prompts') ||
|
||||
path.includes('/generate_images');
|
||||
|
||||
let actualMethod = method;
|
||||
let fetchOptions: RequestInit = {
|
||||
method: actualMethod,
|
||||
headers,
|
||||
credentials: 'include',
|
||||
};
|
||||
|
||||
// For expensive AI POST endpoints, use OPTIONS to check existence without triggering function
|
||||
// If OPTIONS fails, we'll fall back to POST with empty IDs (which fails validation before triggering AI)
|
||||
if (method === 'POST' && isExpensiveAIEndpoint) {
|
||||
actualMethod = 'OPTIONS';
|
||||
fetchOptions.method = 'OPTIONS';
|
||||
// OPTIONS doesn't need body - remove it if present
|
||||
delete (fetchOptions as any).body;
|
||||
} else if (method === 'POST') {
|
||||
// For non-expensive POST endpoints, send invalid data that fails validation early
|
||||
let body: any = {};
|
||||
|
||||
if (path.includes('/test/')) {
|
||||
body = {}; // Test endpoint might accept empty body
|
||||
} else if (path.includes('/login/')) {
|
||||
body = { username: 'test', password: 'test' }; // Will fail validation but endpoint exists
|
||||
} else if (path.includes('/register/')) {
|
||||
body = { username: 'test', email: 'test@test.com', password: 'test' }; // Will fail validation but endpoint exists
|
||||
} else if (path.includes('/bulk_')) {
|
||||
// Bulk operations need ids array
|
||||
body = { ids: [] };
|
||||
} else {
|
||||
// CRUD CREATE operations - minimal valid body
|
||||
body = {};
|
||||
}
|
||||
|
||||
fetchOptions.body = JSON.stringify(body);
|
||||
} else if (method === 'PUT') {
|
||||
// CRUD UPDATE operations - minimal valid body
|
||||
fetchOptions.body = JSON.stringify({});
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(`${API_BASE_URL}${path}`, fetchOptions);
|
||||
} catch (error: any) {
|
||||
// Network error or fetch failed
|
||||
const responseTime = Date.now() - startTime;
|
||||
setEndpointStatuses(prev => ({
|
||||
...prev,
|
||||
[key]: {
|
||||
endpoint: path,
|
||||
method,
|
||||
status: 'error',
|
||||
responseTime,
|
||||
error: error.message || 'Network error',
|
||||
apiStatus: 'error',
|
||||
dataStatus: 'error',
|
||||
},
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
// Determine status based on response
|
||||
let status: 'healthy' | 'warning' | 'error' = 'healthy';
|
||||
let responseText = '';
|
||||
let responseData: any = null;
|
||||
|
||||
// Read response body for debugging and content validation
|
||||
try {
|
||||
responseText = await response.text();
|
||||
// Try to parse JSON to check unified API response format
|
||||
if (responseText && responseText.trim().startsWith('{')) {
|
||||
try {
|
||||
responseData = JSON.parse(responseText);
|
||||
} catch (e) {
|
||||
// Not JSON, ignore
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore body read errors
|
||||
}
|
||||
|
||||
if (actualMethod === 'OPTIONS') {
|
||||
// OPTIONS: 200 with Allow header = healthy, anything else = error
|
||||
if (response.status === 200) {
|
||||
const allowHeader = response.headers.get('Allow') || response.headers.get('allow');
|
||||
if (allowHeader && (allowHeader.includes('POST') || allowHeader.includes('post'))) {
|
||||
status = 'healthy';
|
||||
} else {
|
||||
// If we can't read Allow header (CORS issue), but got 200, assume it's healthy
|
||||
// OPTIONS 200 generally means endpoint exists
|
||||
status = 'healthy';
|
||||
}
|
||||
} else if (response.status === 404) {
|
||||
status = 'error'; // Endpoint doesn't exist
|
||||
} else if (response.status >= 500) {
|
||||
status = 'error';
|
||||
} else {
|
||||
status = 'warning';
|
||||
}
|
||||
} else if (method === 'GET') {
|
||||
// GET: 2xx = healthy, 401/403 = warning (needs auth), 404 = error, 429 = warning (rate limit), 5xx = error
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
// Check unified API response format for errors or empty data
|
||||
if (responseData) {
|
||||
// Check if response has success: false (unified format error)
|
||||
if (responseData.success === false) {
|
||||
status = 'error'; // API returned an error in unified format
|
||||
} else if (responseData.success === true) {
|
||||
// Check for paginated response format (success: true, count: X, results: [...])
|
||||
// or single object response format (success: true, data: {...})
|
||||
const isPaginatedResponse = 'results' in responseData && 'count' in responseData;
|
||||
const isSingleObjectResponse = 'data' in responseData;
|
||||
|
||||
if (isPaginatedResponse) {
|
||||
// Paginated response - check results at top level
|
||||
if (!Array.isArray(responseData.results)) {
|
||||
status = 'warning'; // Missing or invalid results array
|
||||
} else if (responseData.results.length === 0 && responseData.count === 0) {
|
||||
// Empty results with count 0 is OK for list endpoints
|
||||
// Only warn for critical endpoints that should have data
|
||||
if (path.includes('/content_images/') || path.includes('/prompts/by_type/')) {
|
||||
status = 'warning'; // No data available - might indicate configuration issue
|
||||
}
|
||||
}
|
||||
} else if (isSingleObjectResponse) {
|
||||
// Single object response - check data field
|
||||
const shouldHaveData =
|
||||
path.includes('/content_images/') ||
|
||||
path.includes('/prompts/by_type/') ||
|
||||
path.includes('/usage/limits/');
|
||||
|
||||
if (shouldHaveData) {
|
||||
if (responseData.data === null || responseData.data === undefined) {
|
||||
status = 'warning'; // Missing data field
|
||||
} else if (Array.isArray(responseData.data) && responseData.data.length === 0) {
|
||||
if (path.includes('/content_images/') || path.includes('/prompts/by_type/')) {
|
||||
status = 'warning'; // Empty data - might indicate configuration issue
|
||||
}
|
||||
} else if (typeof responseData.data === 'object' && responseData.data !== null) {
|
||||
// Check if it's a nested paginated response
|
||||
if (responseData.data.results && Array.isArray(responseData.data.results) && responseData.data.results.length === 0) {
|
||||
if (path.includes('/content_images/') || path.includes('/prompts/by_type/')) {
|
||||
status = 'warning'; // Empty results - might indicate data issue
|
||||
}
|
||||
} else if (responseData.data.count !== undefined && responseData.data.count === 0) {
|
||||
if (path.includes('/content_images/') || path.includes('/prompts/by_type/')) {
|
||||
status = 'warning'; // No data available - might indicate configuration issue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (!isPaginatedResponse && !isSingleObjectResponse) {
|
||||
// Response has success: true but no data or results
|
||||
// For paginated list endpoints, this is a problem
|
||||
if (path.includes('/prompts/') && !path.includes('/save/') && !path.includes('/by_type/')) {
|
||||
status = 'warning'; // Paginated endpoint missing results field
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If status is still healthy after content checks, keep it healthy
|
||||
if (status === 'healthy') {
|
||||
status = 'healthy'; // HTTP 2xx and valid response = healthy
|
||||
}
|
||||
} else if (response.status === 429) {
|
||||
status = 'warning'; // Rate limited - endpoint exists but temporarily throttled
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
status = 'warning'; // Endpoint exists, needs authentication
|
||||
} else if (response.status === 404) {
|
||||
// For GET requests to specific resource IDs (e.g., /v1/planner/keywords/1/),
|
||||
// 404 is expected and healthy (resource doesn't exist, but endpoint works correctly)
|
||||
// For other GET requests (like list endpoints), 404 means endpoint doesn't exist
|
||||
const isResourceByIdRequest = /\/\d+\/?$/.test(path); // Path ends with /number/ or /number
|
||||
if (method === 'GET' && isResourceByIdRequest) {
|
||||
status = 'healthy'; // GET to specific ID returning 404 is healthy (endpoint exists, resource doesn't)
|
||||
} else {
|
||||
status = 'error'; // Endpoint doesn't exist
|
||||
}
|
||||
} else if (response.status >= 500) {
|
||||
status = 'error';
|
||||
} else {
|
||||
status = 'warning';
|
||||
}
|
||||
} else if (method === 'POST') {
|
||||
// POST: 400 = healthy (endpoint exists and validates), 401/403 = warning, 404 = error, 429 = warning (rate limit), 5xx = error
|
||||
if (response.status === 400) {
|
||||
// 400 means endpoint exists and validation works - this is healthy for monitoring
|
||||
// But check if it's a unified format error response
|
||||
if (responseData && responseData.success === false) {
|
||||
// This is expected for validation errors, so still healthy
|
||||
status = 'healthy';
|
||||
} else {
|
||||
status = 'healthy';
|
||||
}
|
||||
} else if (response.status >= 200 && response.status < 300) {
|
||||
// Check unified API response format for errors
|
||||
if (responseData && responseData.success === false) {
|
||||
status = 'error'; // API returned an error in unified format
|
||||
} else {
|
||||
status = 'healthy';
|
||||
}
|
||||
} else if (response.status === 429) {
|
||||
status = 'warning'; // Rate limited - endpoint exists but temporarily throttled
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
status = 'warning'; // Endpoint exists, needs authentication
|
||||
} else if (response.status === 404) {
|
||||
status = 'error'; // Endpoint doesn't exist
|
||||
} else if (response.status >= 500) {
|
||||
status = 'error';
|
||||
} else {
|
||||
status = 'warning';
|
||||
}
|
||||
} else if (method === 'PUT') {
|
||||
// UPDATE operations
|
||||
if (response.status === 400 || response.status === 404) {
|
||||
// 400/404 expected for test requests (validation/not found) - endpoint is working
|
||||
status = 'healthy';
|
||||
} else if (response.status >= 200 && response.status < 300) {
|
||||
status = 'healthy';
|
||||
} else if (response.status === 429) {
|
||||
status = 'warning'; // Rate limited
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
status = 'warning'; // Needs authentication
|
||||
} else if (response.status >= 500) {
|
||||
status = 'error';
|
||||
} else {
|
||||
status = 'warning';
|
||||
}
|
||||
} else if (method === 'DELETE') {
|
||||
// DELETE operations
|
||||
if (response.status === 204 || response.status === 200) {
|
||||
// 204 No Content or 200 OK - successful delete
|
||||
status = 'healthy';
|
||||
} else if (response.status === 404) {
|
||||
// 404 expected for test requests (resource not found) - endpoint is working
|
||||
status = 'healthy';
|
||||
} else if (response.status === 429) {
|
||||
status = 'warning'; // Rate limited
|
||||
} else if (response.status === 401 || response.status === 403) {
|
||||
status = 'warning'; // Needs authentication
|
||||
} else if (response.status >= 500) {
|
||||
status = 'error';
|
||||
} else {
|
||||
status = 'warning';
|
||||
}
|
||||
}
|
||||
|
||||
// Store API status
|
||||
apiStatus = status;
|
||||
|
||||
// Now check page data population if pageFetchFunction is configured
|
||||
if (endpointConfig?.pageFetchFunction) {
|
||||
try {
|
||||
const pageData = await endpointConfig.pageFetchFunction();
|
||||
|
||||
// Validate data using validator if provided
|
||||
if (endpointConfig.dataValidator) {
|
||||
const isValid = endpointConfig.dataValidator(pageData);
|
||||
if (!isValid) {
|
||||
dataStatus = 'warning'; // Data exists but doesn't pass validation (e.g., empty)
|
||||
} else {
|
||||
dataStatus = 'healthy'; // Data is valid and populated
|
||||
}
|
||||
} else {
|
||||
// If no validator, just check if data exists
|
||||
if (pageData === null || pageData === undefined) {
|
||||
dataStatus = 'error';
|
||||
} else {
|
||||
dataStatus = 'healthy';
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
// Page fetch function failed
|
||||
dataStatus = 'error';
|
||||
console.warn(`[API Monitor] Page data fetch failed for ${path}:`, error.message);
|
||||
}
|
||||
} else {
|
||||
// No page fetch function configured, data status matches API status
|
||||
dataStatus = apiStatus;
|
||||
}
|
||||
|
||||
// Combine API and data statuses - both must be healthy for overall healthy
|
||||
// If either is error, overall is error
|
||||
// If either is warning, overall is warning
|
||||
if (apiStatus === 'error' || dataStatus === 'error') {
|
||||
status = 'error';
|
||||
} else if (apiStatus === 'warning' || dataStatus === 'warning') {
|
||||
status = 'warning';
|
||||
} else {
|
||||
status = 'healthy';
|
||||
}
|
||||
|
||||
// Log warnings/errors for issues detected in response content
|
||||
// Skip logging for expected 400 responses on POST (validation errors are expected)
|
||||
const isExpected400Post = method === 'POST' && response.status === 400;
|
||||
if ((status === 'warning' || status === 'error') && !isExpected400Post) {
|
||||
if (responseData) {
|
||||
if (responseData.success === false) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Unified format error - ${responseData.error || 'Unknown error'}`);
|
||||
} else {
|
||||
// Check for paginated response format
|
||||
const isPaginated = 'results' in responseData && 'count' in responseData;
|
||||
const isSingleObject = 'data' in responseData;
|
||||
|
||||
if (isPaginated) {
|
||||
// Paginated response - check results at top level
|
||||
if (!Array.isArray(responseData.results)) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Missing or invalid results array in paginated response`);
|
||||
} else if (responseData.results.length === 0 && responseData.count === 0 &&
|
||||
(path.includes('/prompts/') && !path.includes('/save/') && !path.includes('/by_type/'))) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Empty paginated response (count: 0, results: [])`);
|
||||
}
|
||||
} else if (isSingleObject) {
|
||||
// Single object response - check data field
|
||||
if (responseData.data === null || responseData.data === undefined) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Missing data field in response`);
|
||||
} else if (Array.isArray(responseData.data) && responseData.data.length === 0) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Empty data array returned`);
|
||||
} else if (responseData.data?.results && Array.isArray(responseData.data.results) && responseData.data.results.length === 0) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Empty results array returned`);
|
||||
} else if (responseData.data?.count === 0) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: No data available (count: 0)`);
|
||||
}
|
||||
} else if (responseData.success === true && !isPaginated && !isSingleObject) {
|
||||
// Response has success: true but no data or results
|
||||
if (path.includes('/prompts/') && !path.includes('/save/') && !path.includes('/by_type/')) {
|
||||
console.warn(`[API Monitor] ${method} ${path}: Paginated endpoint missing results field`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Suppress console errors for expected monitoring responses
|
||||
// Only log real errors (5xx, network errors, or unexpected 4xx for GET endpoints)
|
||||
// Don't log expected 400s for POST endpoints (they indicate validation is working)
|
||||
// Don't log expected 404s for GET requests to specific resource IDs (they indicate endpoint works correctly)
|
||||
const isResourceByIdRequest = /\/\d+\/?$/.test(path); // Path ends with /number/ or /number
|
||||
const isExpectedResponse =
|
||||
(method === 'POST' && response.status === 400) || // Expected validation error
|
||||
(actualMethod === 'OPTIONS' && response.status === 200) || // Expected OPTIONS success
|
||||
(method === 'GET' && response.status >= 200 && response.status < 300 && status === 'healthy') || // Expected GET success with valid data
|
||||
(method === 'GET' && response.status === 404 && isResourceByIdRequest); // Expected 404 for GET to non-existent resource ID
|
||||
|
||||
if (!isExpectedResponse && (response.status >= 500 ||
|
||||
(method === 'GET' && response.status === 404 && !isResourceByIdRequest) ||
|
||||
(actualMethod === 'OPTIONS' && response.status !== 200))) {
|
||||
// These are real errors worth logging
|
||||
console.warn(`[API Monitor] ${method} ${path}: ${response.status}`, responseText.substring(0, 100));
|
||||
}
|
||||
|
||||
setEndpointStatuses(prev => ({
|
||||
...prev,
|
||||
[key]: {
|
||||
endpoint: path,
|
||||
method,
|
||||
status,
|
||||
responseTime,
|
||||
lastChecked: new Date().toISOString(),
|
||||
apiStatus,
|
||||
dataStatus,
|
||||
},
|
||||
}));
|
||||
} catch (err: any) {
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
// Network errors or timeouts are real errors
|
||||
setEndpointStatuses(prev => ({
|
||||
...prev,
|
||||
[key]: {
|
||||
endpoint: path,
|
||||
method,
|
||||
status: 'error',
|
||||
responseTime,
|
||||
lastChecked: new Date().toISOString(),
|
||||
error: err instanceof Error ? err.message : 'Network error',
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
const checkAllEndpoints = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
// Check all endpoints in parallel (but limit concurrency)
|
||||
const allChecks = endpointGroups.flatMap(group =>
|
||||
group.endpoints.map(ep => checkEndpoint(ep.path, ep.method, {
|
||||
pageFetchFunction: ep.pageFetchFunction,
|
||||
dataValidator: ep.dataValidator
|
||||
}))
|
||||
);
|
||||
|
||||
// Check in batches of 5 to avoid overwhelming the server
|
||||
const batchSize = 5;
|
||||
for (let i = 0; i < allChecks.length; i += batchSize) {
|
||||
const batch = allChecks.slice(i, i + batchSize);
|
||||
await Promise.all(batch);
|
||||
// Small delay between batches
|
||||
if (i + batchSize < allChecks.length) {
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}, [checkEndpoint]);
|
||||
|
||||
useEffect(() => {
|
||||
// Only check when component is mounted (page is visible)
|
||||
checkAllEndpoints();
|
||||
|
||||
// Set up auto-refresh if enabled
|
||||
if (autoRefresh) {
|
||||
const interval = setInterval(() => {
|
||||
checkAllEndpoints();
|
||||
}, refreshInterval * 1000);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [autoRefresh, refreshInterval, checkAllEndpoints]);
|
||||
|
||||
const getEndpointStatus = (path: string, method: string): EndpointStatus => {
|
||||
const key = `${method}:${path}`;
|
||||
return endpointStatuses[key] || {
|
||||
endpoint: path,
|
||||
method,
|
||||
status: 'checking',
|
||||
};
|
||||
};
|
||||
|
||||
const getGroupHealth = (group: EndpointGroup) => {
|
||||
const statuses = group.endpoints.map(ep => getEndpointStatus(ep.path, ep.method).status);
|
||||
const healthy = statuses.filter(s => s === 'healthy').length;
|
||||
const warning = statuses.filter(s => s === 'warning').length;
|
||||
const error = statuses.filter(s => s === 'error').length;
|
||||
const total = statuses.length;
|
||||
return { healthy, warning, error, total };
|
||||
};
|
||||
|
||||
const getGroupStatus = (group: EndpointGroup): 'error' | 'warning' | 'healthy' => {
|
||||
const health = getGroupHealth(group);
|
||||
if (health.error > 0) return 'error';
|
||||
if (health.warning > 0) return 'warning';
|
||||
return 'healthy';
|
||||
};
|
||||
|
||||
const getStatusPriority = (status: 'error' | 'warning' | 'healthy'): number => {
|
||||
switch (status) {
|
||||
case 'error': return 0;
|
||||
case 'warning': return 1;
|
||||
case 'healthy': return 2;
|
||||
default: return 3;
|
||||
}
|
||||
};
|
||||
|
||||
// Sort endpoint groups by status (error > warning > healthy)
|
||||
const sortedEndpointGroups = [...endpointGroups].sort((a, b) => {
|
||||
const statusA = getGroupStatus(a);
|
||||
const statusB = getGroupStatus(b);
|
||||
const priorityA = getStatusPriority(statusA);
|
||||
const priorityB = getStatusPriority(statusB);
|
||||
|
||||
// If same priority, sort by name
|
||||
if (priorityA === priorityB) {
|
||||
return a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
return priorityA - priorityB;
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="API Monitor - IGNY8" description="API endpoint monitoring" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Header Controls */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-800 dark:text-white/90">API Monitor</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Monitor API endpoint health and response times
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<label className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={autoRefresh}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.checked;
|
||||
setAutoRefresh(newValue);
|
||||
localStorage.setItem('api-monitor-auto-refresh', String(newValue));
|
||||
}}
|
||||
className="rounded border-gray-300"
|
||||
/>
|
||||
Auto-refresh
|
||||
</label>
|
||||
{autoRefresh && (
|
||||
<select
|
||||
value={refreshInterval}
|
||||
onChange={(e) => {
|
||||
const newValue = Number(e.target.value);
|
||||
setRefreshInterval(newValue);
|
||||
localStorage.setItem('api-monitor-refresh-interval', String(newValue));
|
||||
// Dispatch custom event to notify sidebar component
|
||||
window.dispatchEvent(new Event('api-monitor-interval-changed'));
|
||||
}}
|
||||
className="text-sm rounded border-gray-300 dark:bg-gray-800 dark:border-gray-700"
|
||||
>
|
||||
<option value={30}>30s</option>
|
||||
<option value={60}>1min</option>
|
||||
<option value={300}>5min</option>
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
onClick={checkAllEndpoints}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
|
||||
>
|
||||
{loading ? 'Checking...' : 'Refresh All'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Monitoring Tables - 3 per row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{sortedEndpointGroups.map((group, groupIndex) => {
|
||||
const groupHealth = getGroupHealth(group);
|
||||
const groupStatus = getGroupStatus(group);
|
||||
return (
|
||||
<ComponentCard
|
||||
key={groupIndex}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{group.name}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${getStatusBadge(groupStatus)}`}>
|
||||
{getStatusIcon(groupStatus)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
desc={
|
||||
groupStatus === 'error'
|
||||
? `${groupHealth.error} error${groupHealth.error !== 1 ? 's' : ''}, ${groupHealth.warning} warning${groupHealth.warning !== 1 ? 's' : ''}, ${groupHealth.healthy} healthy`
|
||||
: groupStatus === 'warning'
|
||||
? `${groupHealth.warning} warning${groupHealth.warning !== 1 ? 's' : ''}, ${groupHealth.healthy} healthy`
|
||||
: `${groupHealth.healthy}/${groupHealth.total} healthy`
|
||||
}
|
||||
>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Endpoint
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Status
|
||||
</th>
|
||||
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
|
||||
Time
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{group.endpoints
|
||||
.map((endpoint, epIndex) => ({
|
||||
endpoint,
|
||||
epIndex,
|
||||
status: getEndpointStatus(endpoint.path, endpoint.method),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const priorityA = getStatusPriority(a.status.status);
|
||||
const priorityB = getStatusPriority(b.status.status);
|
||||
// If same priority, sort by method then path
|
||||
if (priorityA === priorityB) {
|
||||
const methodCompare = a.endpoint.method.localeCompare(b.endpoint.method);
|
||||
if (methodCompare !== 0) return methodCompare;
|
||||
return a.endpoint.path.localeCompare(b.endpoint.path);
|
||||
}
|
||||
return priorityA - priorityB;
|
||||
})
|
||||
.map(({ endpoint, epIndex, status }) => {
|
||||
return (
|
||||
<tr key={epIndex} className="hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
||||
<td className="px-3 py-2">
|
||||
<div className="text-xs">
|
||||
<span className="font-mono text-gray-600 dark:text-gray-400">
|
||||
{endpoint.method}
|
||||
</span>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-0.5 truncate max-w-[200px]">
|
||||
{endpoint.path}
|
||||
</div>
|
||||
<div className="text-xs text-gray-400 dark:text-gray-600 mt-0.5">
|
||||
{endpoint.description}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 text-xs px-2 py-1 rounded ${getStatusBadge(status.status)}`}
|
||||
title={status.error || status.status}
|
||||
>
|
||||
<span>{getStatusIcon(status.status)}</span>
|
||||
<span className="capitalize">{status.status}</span>
|
||||
</span>
|
||||
{status.apiStatus && status.dataStatus && endpoint.pageFetchFunction && status.apiStatus !== status.dataStatus && (
|
||||
<div className="text-xs space-y-0.5 mt-1">
|
||||
<div className={`${getStatusColor(status.apiStatus)}`}>
|
||||
API: {status.apiStatus}
|
||||
</div>
|
||||
<div className={`${getStatusColor(status.dataStatus)}`}>
|
||||
Data: {status.dataStatus}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-3 py-2">
|
||||
{status.responseTime ? (
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">
|
||||
{status.responseTime}ms
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-xs text-gray-400 dark:text-gray-600">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<ComponentCard title="Summary" desc="Overall API health statistics">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
{sortedEndpointGroups.map((group, index) => {
|
||||
const groupHealth = getGroupHealth(group);
|
||||
const groupStatus = getGroupStatus(group);
|
||||
const percentage = groupHealth.total > 0
|
||||
? Math.round((groupHealth.healthy / groupHealth.total) * 100)
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div key={index} className="text-center">
|
||||
<div className={`text-2xl font-semibold ${getStatusColor(groupStatus)}`}>
|
||||
{percentage}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-1 flex items-center justify-center gap-1">
|
||||
<span>{group.name}</span>
|
||||
<span className={getStatusColor(groupStatus)}>{getStatusIcon(groupStatus)}</span>
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">
|
||||
{groupHealth.healthy}/{groupHealth.total} healthy
|
||||
{groupHealth.error > 0 && ` • ${groupHealth.error} error${groupHealth.error !== 1 ? 's' : ''}`}
|
||||
{groupHealth.warning > 0 && ` • ${groupHealth.warning} warning${groupHealth.warning !== 1 ? 's' : ''}`}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,966 +0,0 @@
|
||||
import React, { useState, useEffect, useCallback, useRef } from "react";
|
||||
import { AlertTriangle } from "lucide-react";
|
||||
import PageMeta from "../../components/common/PageMeta";
|
||||
import ComponentCard from "../../components/common/ComponentCard";
|
||||
import DebugSiteSelector from "../../components/common/DebugSiteSelector";
|
||||
import WordPressIntegrationDebug from "./WordPressIntegrationDebug";
|
||||
import { API_BASE_URL, fetchAPI } from "../../services/api";
|
||||
import { useSiteStore } from "../../store/siteStore";
|
||||
import { useToast } from "../../components/ui/toast/ToastContainer";
|
||||
|
||||
interface HealthCheck {
|
||||
name: string;
|
||||
description: string;
|
||||
status: 'healthy' | 'warning' | 'error' | 'checking';
|
||||
message?: string;
|
||||
details?: string;
|
||||
lastChecked?: string;
|
||||
step?: string;
|
||||
payloadKeys?: string[];
|
||||
responseCode?: number;
|
||||
}
|
||||
|
||||
interface ModuleHealth {
|
||||
module: string;
|
||||
description: string;
|
||||
checks: HealthCheck[];
|
||||
}
|
||||
|
||||
interface SyncEvent {
|
||||
id: string;
|
||||
timestamp: string;
|
||||
direction: '📤 IGNY8 → WP' | '📥 WP → IGNY8';
|
||||
trigger: string;
|
||||
contentId?: number;
|
||||
taskId?: number;
|
||||
status: 'success' | 'partial' | 'failed';
|
||||
steps: SyncEventStep[];
|
||||
payload?: any;
|
||||
response?: any;
|
||||
}
|
||||
|
||||
interface SyncEventStep {
|
||||
step: string;
|
||||
file: string;
|
||||
status: 'success' | 'warning' | 'failed' | 'skipped';
|
||||
details: string;
|
||||
error?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
interface DataSyncValidation {
|
||||
field: string;
|
||||
sentByIgny8: boolean;
|
||||
receivedByWP: boolean;
|
||||
storedInWP: boolean;
|
||||
verifiedInWPPost: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
interface IntegrationHealth {
|
||||
overall: 'healthy' | 'warning' | 'error';
|
||||
lastSyncIgny8ToWP?: string;
|
||||
lastSyncWPToIgny8?: string;
|
||||
lastSiteMetadataCheck?: string;
|
||||
wpApiReachable: boolean;
|
||||
wpStatusEndpoint: boolean;
|
||||
wpMetadataEndpoint: boolean;
|
||||
apiKeyValid: boolean;
|
||||
jwtTokenValid: boolean;
|
||||
}
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
case 'success': return 'text-green-600 dark:text-green-400';
|
||||
case 'warning':
|
||||
case 'partial': return 'text-yellow-600 dark:text-yellow-400';
|
||||
case 'error':
|
||||
case 'failed': return 'text-red-600 dark:text-red-400';
|
||||
case 'checking': return 'text-blue-600 dark:text-blue-400';
|
||||
case 'skipped': return 'text-gray-600 dark:text-gray-400';
|
||||
default: return 'text-gray-600 dark:text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
case 'success': return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400';
|
||||
case 'warning':
|
||||
case 'partial': return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400';
|
||||
case 'error':
|
||||
case 'failed': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
|
||||
case 'checking': return 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400';
|
||||
case 'skipped': return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
|
||||
default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'healthy':
|
||||
case 'success': return '✓';
|
||||
case 'warning':
|
||||
case 'partial': return '⚠';
|
||||
case 'error':
|
||||
case 'failed': return '✗';
|
||||
case 'checking': return '⟳';
|
||||
case 'skipped': return '—';
|
||||
default: return '?';
|
||||
}
|
||||
};
|
||||
|
||||
export default function DebugStatus() {
|
||||
const { activeSite } = useSiteStore();
|
||||
const toast = useToast();
|
||||
|
||||
// Tab navigation state
|
||||
const [activeTab, setActiveTab] = useState<'system-health' | 'wp-integration'>('system-health');
|
||||
|
||||
// Data state
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [moduleHealths, setModuleHealths] = useState<ModuleHealth[]>([]);
|
||||
const [debugEnabled, setDebugEnabled] = useState(false);
|
||||
|
||||
// Helper to call API endpoints
|
||||
const apiCall = useCallback(async (endpoint: string, options: RequestInit = {}) => {
|
||||
try {
|
||||
console.log(`[DEBUG] Calling API: ${endpoint}`);
|
||||
// fetchAPI returns parsed JSON data directly (not Response object)
|
||||
const data = await fetchAPI(endpoint, options);
|
||||
console.log(`[DEBUG] Success from ${endpoint}:`, data);
|
||||
// Return mock response object with data
|
||||
return {
|
||||
response: { ok: true, status: 200, statusText: 'OK' } as Response,
|
||||
data
|
||||
};
|
||||
} catch (error: any) {
|
||||
console.error(`[DEBUG] API call failed for ${endpoint}:`, error);
|
||||
// fetchAPI throws errors for non-OK responses - extract error data
|
||||
const errorData = error.response || {
|
||||
detail: error.message?.replace(/^API Error.*?:\s*/, '') || 'Request failed'
|
||||
};
|
||||
const status = error.status || 500;
|
||||
console.log(`[DEBUG] Error from ${endpoint}:`, { status, errorData });
|
||||
return {
|
||||
response: { ok: false, status, statusText: error.message } as Response,
|
||||
data: errorData
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Check database schema field mappings (the issues we just fixed)
|
||||
const checkDatabaseSchemaMapping = useCallback(async (): Promise<HealthCheck> => {
|
||||
if (!activeSite) {
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'warning',
|
||||
message: 'No site selected',
|
||||
details: 'Please select a site to run health checks',
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
// Test Writer Content endpoint (was failing with entity_type error)
|
||||
const { response: contentResp, data: contentData } = await apiCall(`/v1/writer/content/?site=${activeSite.id}`);
|
||||
|
||||
if (!contentResp.ok) {
|
||||
const errorMsg = contentData?.detail || contentData?.error || contentData?.message || `HTTP ${contentResp.status}`;
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'error',
|
||||
message: errorMsg,
|
||||
details: 'Cannot verify schema - API endpoint failed',
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
// Check if response has the expected structure with new field names
|
||||
if (contentData?.results && Array.isArray(contentData.results)) {
|
||||
// If we can fetch content, schema mapping is working
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'healthy',
|
||||
message: 'All model fields correctly mapped via db_column attributes',
|
||||
details: `Content API working correctly for ${activeSite.name}. Fields like content_type, content_html, content_structure are properly mapped.`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'warning',
|
||||
message: 'Content API returned unexpected structure',
|
||||
details: 'Response format may have changed',
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
name: 'Database Schema Mapping',
|
||||
description: 'Checks if model field names map correctly to database columns',
|
||||
status: 'error',
|
||||
message: error.message || 'Failed to check schema mapping',
|
||||
details: 'Check if db_column attributes are set correctly in models',
|
||||
lastChecked: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}, [apiCall, activeSite]);
|
||||
|
||||
// Check Writer module health
|
||||
const checkWriterModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
if (!activeSite) {
|
||||
checks.push({
|
||||
name: 'Content List',
|
||||
description: 'Writer content listing endpoint',
|
||||
status: 'warning',
|
||||
message: 'No site selected',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
return checks;
|
||||
}
|
||||
|
||||
// Check Content endpoint
|
||||
try {
|
||||
const { response: contentResp, data: contentData } = await apiCall(`/v1/writer/content/?site=${activeSite.id}`);
|
||||
|
||||
if (contentResp && contentResp.ok) {
|
||||
checks.push({
|
||||
name: 'Content List',
|
||||
description: 'Writer content listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${contentData?.count || contentData?.results?.length || 0} content items for ${activeSite.name}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
const errorMsg = contentData?.detail || contentData?.error || contentData?.message || (contentResp ? `HTTP ${contentResp.status}` : 'Request failed');
|
||||
checks.push({
|
||||
name: 'Content List',
|
||||
description: 'Writer content listing endpoint',
|
||||
status: 'error',
|
||||
message: errorMsg,
|
||||
details: 'Check authentication and endpoint availability',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Content List',
|
||||
description: 'Writer content listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check Tasks endpoint
|
||||
try {
|
||||
const { response: tasksResp, data: tasksData } = await apiCall(`/v1/writer/tasks/?site=${activeSite.id}`);
|
||||
|
||||
if (tasksResp && tasksResp.ok) {
|
||||
checks.push({
|
||||
name: 'Tasks List',
|
||||
description: 'Writer tasks listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${tasksData?.count || tasksData?.results?.length || 0} tasks for ${activeSite.name}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
const errorMsg = tasksData?.detail || tasksData?.error || tasksData?.message || (tasksResp ? `HTTP ${tasksResp.status}` : 'Request failed');
|
||||
checks.push({
|
||||
name: 'Tasks List',
|
||||
description: 'Writer tasks listing endpoint',
|
||||
status: 'error',
|
||||
message: errorMsg,
|
||||
details: 'Check authentication and endpoint availability',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Tasks List',
|
||||
description: 'Writer tasks listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check Content Validation
|
||||
try {
|
||||
// Get first content ID if available
|
||||
const { data: contentData } = await apiCall(`/v1/writer/content/?site=${activeSite.id}`);
|
||||
const firstContentId = contentData?.results?.[0]?.id;
|
||||
|
||||
if (firstContentId) {
|
||||
const { response: validationResp, data: validationData } = await apiCall(
|
||||
`/v1/writer/content/${firstContentId}/validation/`
|
||||
);
|
||||
|
||||
if (validationResp.ok && validationData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Content Validation',
|
||||
description: 'Content validation before publish',
|
||||
status: 'healthy',
|
||||
message: 'Validation endpoint working',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content Validation',
|
||||
description: 'Content validation before publish',
|
||||
status: 'error',
|
||||
message: validationData?.error || `Failed with ${validationResp.status}`,
|
||||
details: 'Check validation_service.py field references (content_html, content_type)',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content Validation',
|
||||
description: 'Content validation before publish',
|
||||
status: 'warning',
|
||||
message: 'No content available to test validation',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Content Validation',
|
||||
description: 'Content validation before publish',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, [apiCall, activeSite]);
|
||||
|
||||
// Check Planner module health
|
||||
const checkPlannerModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
if (!activeSite) {
|
||||
checks.push({
|
||||
name: 'Keyword Clusters',
|
||||
description: 'Planner keyword clustering endpoint',
|
||||
status: 'warning',
|
||||
message: 'No site selected',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
return checks;
|
||||
}
|
||||
|
||||
// Check Keyword Clusters endpoint
|
||||
try {
|
||||
const { response: clustersResp, data: clustersData } = await apiCall(`/v1/planner/clusters/?site=${activeSite.id}`);
|
||||
|
||||
if (clustersResp && clustersResp.ok) {
|
||||
checks.push({
|
||||
name: 'Clusters List',
|
||||
description: 'Planner clusters listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${clustersData?.count || clustersData?.results?.length || 0} clusters for ${activeSite.name}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
const errorMsg = clustersData?.detail || clustersData?.error || clustersData?.message || (clustersResp ? `HTTP ${clustersResp.status}` : 'Request failed');
|
||||
checks.push({
|
||||
name: 'Clusters List',
|
||||
description: 'Planner clusters listing endpoint',
|
||||
status: 'error',
|
||||
message: errorMsg,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Clusters List',
|
||||
description: 'Planner clusters listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check Keywords endpoint
|
||||
try {
|
||||
const { response: keywordsResp, data: keywordsData } = await apiCall(`/v1/planner/keywords/?site=${activeSite.id}`);
|
||||
|
||||
if (keywordsResp.ok && keywordsData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Keywords List',
|
||||
description: 'Planner keywords listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${keywordsData?.count || 0} keywords for ${activeSite.name}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Keywords List',
|
||||
description: 'Planner keywords listing endpoint',
|
||||
status: 'error',
|
||||
message: keywordsData?.error || `Failed with ${keywordsResp.status}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Keywords List',
|
||||
description: 'Planner keywords listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
// Check Ideas endpoint
|
||||
try {
|
||||
const { response: ideasResp, data: ideasData } = await apiCall(`/v1/planner/ideas/?site=${activeSite.id}`);
|
||||
|
||||
if (ideasResp.ok && ideasData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Ideas List',
|
||||
description: 'Planner ideas listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${ideasData?.count || 0} ideas for ${activeSite.name}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Ideas List',
|
||||
description: 'Planner ideas listing endpoint',
|
||||
status: 'error',
|
||||
message: ideasData?.error || `Failed with ${ideasResp.status}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Ideas List',
|
||||
description: 'Planner ideas listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, [apiCall, activeSite]);
|
||||
|
||||
// Check Sites module health
|
||||
const checkSitesModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
// Check Sites list
|
||||
try {
|
||||
const { response: sitesResp, data: sitesData } = await apiCall('/v1/auth/sites/');
|
||||
|
||||
if (sitesResp && sitesResp.ok) {
|
||||
checks.push({
|
||||
name: 'Sites List',
|
||||
description: 'Sites listing endpoint',
|
||||
status: 'healthy',
|
||||
message: `Found ${sitesData?.count || sitesData?.results?.length || sitesData?.length || 0} sites`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
const errorMsg = sitesData?.detail || sitesData?.error || sitesData?.message || (sitesResp ? `HTTP ${sitesResp.status}` : 'Request failed');
|
||||
checks.push({
|
||||
name: 'Sites List',
|
||||
description: 'Sites listing endpoint',
|
||||
status: 'error',
|
||||
message: errorMsg,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Sites List',
|
||||
description: 'Sites listing endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, [apiCall]);
|
||||
|
||||
// Check Integration module health
|
||||
const checkIntegrationModule = useCallback(async (): Promise<HealthCheck[]> => {
|
||||
const checks: HealthCheck[] = [];
|
||||
|
||||
if (!activeSite) {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'warning',
|
||||
message: 'No site selected',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
return checks;
|
||||
}
|
||||
|
||||
// Check Integration content types endpoint
|
||||
try {
|
||||
// First get the integration for this site
|
||||
const { response: intResp, data: intData } = await apiCall(
|
||||
`/v1/integration/integrations/?site_id=${activeSite.id}`
|
||||
);
|
||||
|
||||
if (!intResp.ok || !intData || (Array.isArray(intData) && intData.length === 0) || (intData.results && intData.results.length === 0)) {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'warning',
|
||||
message: 'No WordPress integration configured',
|
||||
details: 'Add a WordPress integration in Settings > Integrations',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
// Extract integration ID from response
|
||||
const integration = Array.isArray(intData) ? intData[0] : (intData.results ? intData.results[0] : intData);
|
||||
const integrationId = integration?.id;
|
||||
|
||||
if (!integrationId) {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'error',
|
||||
message: 'Invalid integration data',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
// Now check content types for this integration
|
||||
const { response: contentTypesResp, data: contentTypesData } = await apiCall(
|
||||
`/v1/integration/integrations/${integrationId}/content-types/`
|
||||
);
|
||||
|
||||
if (contentTypesResp.ok && contentTypesData?.success !== false) {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'healthy',
|
||||
message: `Content types synced for ${activeSite.name}`,
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
} else {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'error',
|
||||
message: contentTypesData?.detail || contentTypesData?.error || `Failed with ${contentTypesResp.status}`,
|
||||
details: 'Check integration views field mappings (content_type_map vs entity_type_map)',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
checks.push({
|
||||
name: 'Content Types Sync',
|
||||
description: 'Integration content types endpoint',
|
||||
status: 'error',
|
||||
message: error.message || 'Network error',
|
||||
lastChecked: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return checks;
|
||||
}, [apiCall, activeSite]);
|
||||
|
||||
// Run all health checks
|
||||
const runAllChecks = useCallback(async () => {
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Run schema check first
|
||||
const schemaCheck = await checkDatabaseSchemaMapping();
|
||||
|
||||
// Run module checks in parallel
|
||||
const [writerChecks, plannerChecks, sitesChecks, integrationChecks] = await Promise.all([
|
||||
checkWriterModule(),
|
||||
checkPlannerModule(),
|
||||
checkSitesModule(),
|
||||
checkIntegrationModule(),
|
||||
]);
|
||||
|
||||
// Build module health results
|
||||
const moduleHealthResults: ModuleHealth[] = [
|
||||
{
|
||||
module: 'Database Schema',
|
||||
description: 'Critical database field mapping checks',
|
||||
checks: [schemaCheck],
|
||||
},
|
||||
{
|
||||
module: 'Writer Module',
|
||||
description: 'Content creation and task management',
|
||||
checks: writerChecks,
|
||||
},
|
||||
{
|
||||
module: 'Planner Module',
|
||||
description: 'Keyword clustering and content planning',
|
||||
checks: plannerChecks,
|
||||
},
|
||||
{
|
||||
module: 'Sites Module',
|
||||
description: 'Site management and configuration',
|
||||
checks: sitesChecks,
|
||||
},
|
||||
{
|
||||
module: 'Integration Module',
|
||||
description: 'External platform sync (WordPress, etc.)',
|
||||
checks: integrationChecks,
|
||||
},
|
||||
];
|
||||
|
||||
setModuleHealths(moduleHealthResults);
|
||||
} catch (error) {
|
||||
console.error('Failed to run health checks:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [
|
||||
apiCall,
|
||||
checkDatabaseSchemaMapping,
|
||||
checkWriterModule,
|
||||
checkPlannerModule,
|
||||
checkSitesModule,
|
||||
checkIntegrationModule,
|
||||
]);
|
||||
|
||||
// Run checks on mount and when site changes
|
||||
useEffect(() => {
|
||||
if (!debugEnabled || !activeSite) {
|
||||
setModuleHealths([]);
|
||||
return;
|
||||
}
|
||||
runAllChecks();
|
||||
}, [runAllChecks, debugEnabled, activeSite]);
|
||||
|
||||
// Calculate module status
|
||||
const getModuleStatus = (module: ModuleHealth): 'error' | 'warning' | 'healthy' => {
|
||||
const statuses = module.checks.map(c => c.status);
|
||||
if (statuses.some(s => s === 'error')) return 'error';
|
||||
if (statuses.some(s => s === 'warning')) return 'warning';
|
||||
return 'healthy';
|
||||
};
|
||||
|
||||
// Calculate overall health
|
||||
const getOverallHealth = () => {
|
||||
const allStatuses = moduleHealths.flatMap(m => m.checks.map(c => c.status));
|
||||
const total = allStatuses.length;
|
||||
const healthy = allStatuses.filter(s => s === 'healthy').length;
|
||||
const warning = allStatuses.filter(s => s === 'warning').length;
|
||||
const error = allStatuses.filter(s => s === 'error').length;
|
||||
|
||||
let status: 'error' | 'warning' | 'healthy' = 'healthy';
|
||||
if (error > 0) status = 'error';
|
||||
else if (warning > 0) status = 'warning';
|
||||
|
||||
return { total, healthy, warning, error, status, percentage: total > 0 ? Math.round((healthy / total) * 100) : 0 };
|
||||
};
|
||||
|
||||
const overallHealth = getOverallHealth();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="Debug Status - IGNY8" description="Module health checks and diagnostics" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-800 dark:text-white/90">Debug Status</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Comprehensive health checks for all modules and recent bug fixes
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={runAllChecks}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
|
||||
>
|
||||
{loading ? 'Running Checks...' : 'Refresh All'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Site Selector & Debug Toggle Combined */}
|
||||
<div className="bg-white dark:bg-gray-800 shadow rounded-lg p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<DebugSiteSelector />
|
||||
{activeSite && (
|
||||
<label className="flex items-center space-x-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={debugEnabled}
|
||||
onChange={(e) => setDebugEnabled(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{debugEnabled ? 'Debug Enabled' : 'Debug Disabled'}
|
||||
</span>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* No Site Selected Warning */}
|
||||
{!activeSite && (
|
||||
<div className="bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-900/50 rounded-lg p-4">
|
||||
<div className="flex items-start space-x-3">
|
||||
<AlertTriangle className="h-5 w-5 text-yellow-500 mt-0.5" />
|
||||
<div>
|
||||
<p className="text-sm text-yellow-800 dark:text-yellow-200 font-medium">No Site Selected</p>
|
||||
<p className="text-xs text-yellow-600 dark:text-yellow-300 mt-1">
|
||||
Please select a site above to run health checks and view debug information.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tab Content */}
|
||||
{debugEnabled && activeSite ? (
|
||||
<>
|
||||
{/* Tab Navigation */}
|
||||
<div className="bg-white dark:bg-gray-800 shadow rounded-lg">
|
||||
<div className="border-b border-gray-200 dark:border-gray-700">
|
||||
<nav className="flex space-x-8 px-6">
|
||||
<button
|
||||
onClick={() => setActiveTab('system-health')}
|
||||
className={`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
|
||||
activeTab === 'system-health'
|
||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
System Health
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('wp-integration')}
|
||||
className={`py-4 px-2 border-b-2 font-medium text-sm transition-colors ${
|
||||
activeTab === 'wp-integration'
|
||||
? 'border-blue-500 text-blue-600 dark:text-blue-400'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
}`}
|
||||
>
|
||||
IGNY8 ↔ WordPress
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{activeTab === 'system-health' ? (
|
||||
<div className="space-y-6">
|
||||
{/* Overall Health Summary */}
|
||||
<ComponentCard
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>Overall System Health</span>
|
||||
<span className={`text-lg ${getStatusColor(overallHealth.status)}`}>
|
||||
{getStatusIcon(overallHealth.status)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
desc={
|
||||
overallHealth.status === 'error'
|
||||
? `${overallHealth.error} critical issue${overallHealth.error !== 1 ? 's' : ''} detected`
|
||||
: overallHealth.status === 'warning'
|
||||
? `${overallHealth.warning} warning${overallHealth.warning !== 1 ? 's' : ''} detected`
|
||||
: 'All systems operational'
|
||||
}
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Health Percentage */}
|
||||
<div className="text-center">
|
||||
<div className={`text-5xl font-bold ${getStatusColor(overallHealth.status)}`}>
|
||||
{overallHealth.percentage}%
|
||||
</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 mt-2">
|
||||
{overallHealth.healthy} of {overallHealth.total} checks passed
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Health Breakdown */}
|
||||
<div className="grid grid-cols-3 gap-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div className="text-center">
|
||||
<div className={`text-2xl font-semibold ${getStatusColor('healthy')}`}>
|
||||
{overallHealth.healthy}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">Healthy</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-2xl font-semibold ${getStatusColor('warning')}`}>
|
||||
{overallHealth.warning}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">Warnings</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className={`text-2xl font-semibold ${getStatusColor('error')}`}>
|
||||
{overallHealth.error}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-500 mt-1">Errors</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
{/* Module Health Cards */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{moduleHealths.map((moduleHealth, index) => {
|
||||
const moduleStatus = getModuleStatus(moduleHealth);
|
||||
const healthyCount = moduleHealth.checks.filter(c => c.status === 'healthy').length;
|
||||
const totalCount = moduleHealth.checks.length;
|
||||
|
||||
return (
|
||||
<ComponentCard
|
||||
key={index}
|
||||
title={
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{moduleHealth.module}</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${getStatusBadge(moduleStatus)}`}>
|
||||
{getStatusIcon(moduleStatus)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
desc={
|
||||
moduleStatus === 'error'
|
||||
? `Issues detected - ${healthyCount}/${totalCount} checks passed`
|
||||
: moduleStatus === 'warning'
|
||||
? `Warnings detected - ${healthyCount}/${totalCount} checks passed`
|
||||
: `All ${totalCount} checks passed`
|
||||
}
|
||||
>
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-4">
|
||||
{moduleHealth.description}
|
||||
</p>
|
||||
|
||||
{/* Health Checks List */}
|
||||
{moduleHealth.checks.map((check, checkIndex) => (
|
||||
<div
|
||||
key={checkIndex}
|
||||
className={`p-3 rounded-lg border ${
|
||||
check.status === 'healthy'
|
||||
? 'border-green-200 dark:border-green-900/50 bg-green-50 dark:bg-green-900/20'
|
||||
: check.status === 'warning'
|
||||
? 'border-yellow-200 dark:border-yellow-900/50 bg-yellow-50 dark:bg-yellow-900/20'
|
||||
: check.status === 'error'
|
||||
? 'border-red-200 dark:border-red-900/50 bg-red-50 dark:bg-red-900/20'
|
||||
: 'border-blue-200 dark:border-blue-900/50 bg-blue-50 dark:bg-blue-900/20'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<span className={`text-lg ${getStatusColor(check.status)} flex-shrink-0`}>
|
||||
{getStatusIcon(check.status)}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium text-gray-800 dark:text-white/90">
|
||||
{check.name}
|
||||
</span>
|
||||
<span className={`text-xs px-2 py-0.5 rounded ${getStatusBadge(check.status)}`}>
|
||||
{check.status}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
||||
{check.description}
|
||||
</p>
|
||||
{check.message && (
|
||||
<p className={`text-sm ${getStatusColor(check.status)} font-medium`}>
|
||||
{check.message}
|
||||
</p>
|
||||
)}
|
||||
{check.details && (
|
||||
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1 italic">
|
||||
💡 {check.details}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Help Section */}
|
||||
<ComponentCard
|
||||
title="Troubleshooting Guide"
|
||||
desc="Common issues and solutions"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-200 dark:border-blue-900/50">
|
||||
<h3 className="font-medium text-gray-800 dark:text-white/90 mb-2">
|
||||
Database Schema Mapping Errors
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
If you see errors about missing fields like <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">content_type</code>,
|
||||
<code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs ml-1">content_structure</code>, or
|
||||
<code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs ml-1">content_html</code>:
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-400 list-disc list-inside space-y-1">
|
||||
<li>Check that model fields match database column names</li>
|
||||
<li>Verify database columns exist with <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">SELECT column_name FROM information_schema.columns</code></li>
|
||||
<li>All field names now match database (no db_column mappings)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-200 dark:border-yellow-900/50">
|
||||
<h3 className="font-medium text-gray-800 dark:text-white/90 mb-2">
|
||||
Field Reference Errors in Code
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">
|
||||
If API endpoints return 500 errors with AttributeError or similar:
|
||||
</p>
|
||||
<ul className="text-sm text-gray-600 dark:text-gray-400 list-disc list-inside space-y-1">
|
||||
<li>All field names now standardized: <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">content_type</code>, <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">content_structure</code>, <code className="px-1 py-0.5 bg-gray-200 dark:bg-gray-700 rounded text-xs">content_html</code></li>
|
||||
<li>Old names removed: entity_type, site_entity_type, cluster_role, html_content</li>
|
||||
<li>Check views, services, and serializers in writer/planner/integration modules</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-200 dark:border-green-900/50">
|
||||
<h3 className="font-medium text-gray-800 dark:text-white/90 mb-2">
|
||||
All Checks Passing?
|
||||
</h3>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Great! Your system is healthy. This page will help you quickly diagnose issues if they appear in the future.
|
||||
Bookmark this page and check it first when troubleshooting module-specific problems.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
) : (
|
||||
// WordPress Integration Debug Tab
|
||||
<WordPressIntegrationDebug />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="bg-gray-50 dark:bg-gray-800/50 border border-gray-200 dark:border-gray-700 rounded-lg p-6 text-center">
|
||||
<AlertTriangle className="h-8 w-8 text-gray-400 mx-auto mb-2" />
|
||||
<p className="text-gray-600 dark:text-gray-400">
|
||||
{activeSite
|
||||
? 'Enable debug mode above to view system health checks'
|
||||
: 'Select a site and enable debug mode to view system health checks'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,569 +0,0 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import {
|
||||
Activity,
|
||||
Zap,
|
||||
Database,
|
||||
Server,
|
||||
Workflow,
|
||||
Globe,
|
||||
CheckCircle,
|
||||
AlertTriangle,
|
||||
XCircle,
|
||||
RefreshCw,
|
||||
TrendingUp,
|
||||
Clock,
|
||||
Cpu,
|
||||
HardDrive,
|
||||
MemoryStick
|
||||
} from 'lucide-react';
|
||||
import PageMeta from '../../components/common/PageMeta';
|
||||
import DebugSiteSelector from '../../components/common/DebugSiteSelector';
|
||||
import { fetchAPI } from '../../services/api';
|
||||
import { useSiteStore } from '../../store/siteStore';
|
||||
|
||||
// Types
|
||||
interface SystemMetrics {
|
||||
cpu: { usage_percent: number; cores: number; status: string };
|
||||
memory: { total_gb: number; used_gb: number; usage_percent: number; status: string };
|
||||
disk: { total_gb: number; used_gb: number; usage_percent: number; status: string };
|
||||
}
|
||||
|
||||
interface ServiceHealth {
|
||||
database: { status: string; connected: boolean };
|
||||
redis: { status: string; connected: boolean };
|
||||
celery: { status: string; worker_count: number };
|
||||
}
|
||||
|
||||
interface ApiGroupHealth {
|
||||
name: string;
|
||||
total: number;
|
||||
healthy: number;
|
||||
warning: number;
|
||||
error: number;
|
||||
percentage: number;
|
||||
status: 'healthy' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
interface WorkflowHealth {
|
||||
name: string;
|
||||
steps: { name: string; status: 'healthy' | 'warning' | 'error'; message?: string }[];
|
||||
overall: 'healthy' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
interface IntegrationHealth {
|
||||
platform: string;
|
||||
connected: boolean;
|
||||
last_sync: string | null;
|
||||
sync_enabled: boolean;
|
||||
plugin_active: boolean;
|
||||
status: 'healthy' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
export default function MasterStatus() {
|
||||
const { activeSite } = useSiteStore();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [lastUpdate, setLastUpdate] = useState<Date>(new Date());
|
||||
|
||||
// System metrics
|
||||
const [systemMetrics, setSystemMetrics] = useState<SystemMetrics | null>(null);
|
||||
const [serviceHealth, setServiceHealth] = useState<ServiceHealth | null>(null);
|
||||
|
||||
// API health
|
||||
const [apiHealth, setApiHealth] = useState<ApiGroupHealth[]>([]);
|
||||
|
||||
// Workflow health (keywords → clusters → ideas → tasks → content → publish)
|
||||
const [workflowHealth, setWorkflowHealth] = useState<WorkflowHealth[]>([]);
|
||||
|
||||
// Integration health
|
||||
const [integrationHealth, setIntegrationHealth] = useState<IntegrationHealth | null>(null);
|
||||
|
||||
// Fetch system metrics
|
||||
const fetchSystemMetrics = useCallback(async () => {
|
||||
try {
|
||||
const data = await fetchAPI('/v1/system/status/');
|
||||
setSystemMetrics(data.system);
|
||||
setServiceHealth({
|
||||
database: data.database,
|
||||
redis: data.redis,
|
||||
celery: data.celery,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch system metrics:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Fetch API health (aggregated from API monitor)
|
||||
const fetchApiHealth = useCallback(async () => {
|
||||
const groups = [
|
||||
{ name: 'Auth & User', endpoints: ['/v1/auth/me/', '/v1/auth/sites/', '/v1/auth/accounts/'] },
|
||||
{ name: 'Planner', endpoints: ['/v1/planner/keywords/', '/v1/planner/clusters/', '/v1/planner/ideas/'] },
|
||||
{ name: 'Writer', endpoints: ['/v1/writer/tasks/', '/v1/writer/content/', '/v1/writer/images/'] },
|
||||
{ name: 'Integration', endpoints: ['/v1/integration/integrations/'] },
|
||||
];
|
||||
|
||||
const healthChecks: ApiGroupHealth[] = [];
|
||||
|
||||
for (const group of groups) {
|
||||
let healthy = 0;
|
||||
let warning = 0;
|
||||
let error = 0;
|
||||
|
||||
for (const endpoint of group.endpoints) {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
await fetchAPI(endpoint + (activeSite ? `?site=${activeSite.id}` : ''));
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
if (responseTime < 1000) healthy++;
|
||||
else if (responseTime < 3000) warning++;
|
||||
else error++;
|
||||
} catch (e) {
|
||||
error++;
|
||||
}
|
||||
}
|
||||
|
||||
const total = group.endpoints.length;
|
||||
const percentage = Math.round((healthy / total) * 100);
|
||||
|
||||
healthChecks.push({
|
||||
name: group.name,
|
||||
total,
|
||||
healthy,
|
||||
warning,
|
||||
error,
|
||||
percentage,
|
||||
status: error > 0 ? 'error' : warning > 0 ? 'warning' : 'healthy',
|
||||
});
|
||||
}
|
||||
|
||||
setApiHealth(healthChecks);
|
||||
}, [activeSite]);
|
||||
|
||||
// Check workflow health (end-to-end pipeline)
|
||||
const checkWorkflowHealth = useCallback(async () => {
|
||||
if (!activeSite) {
|
||||
setWorkflowHealth([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const workflows: WorkflowHealth[] = [];
|
||||
|
||||
// Content Generation Workflow
|
||||
try {
|
||||
const steps = [];
|
||||
|
||||
// Step 1: Keywords exist
|
||||
const keywords = await fetchAPI(`/v1/planner/keywords/?site=${activeSite.id}`);
|
||||
steps.push({
|
||||
name: 'Keywords Imported',
|
||||
status: keywords.count > 0 ? 'healthy' : 'warning' as const,
|
||||
message: `${keywords.count} keywords`,
|
||||
});
|
||||
|
||||
// Step 2: Clusters exist
|
||||
const clusters = await fetchAPI(`/v1/planner/clusters/?site=${activeSite.id}`);
|
||||
steps.push({
|
||||
name: 'Content Clusters',
|
||||
status: clusters.count > 0 ? 'healthy' : 'warning' as const,
|
||||
message: `${clusters.count} clusters`,
|
||||
});
|
||||
|
||||
// Step 3: Ideas generated
|
||||
const ideas = await fetchAPI(`/v1/planner/ideas/?site=${activeSite.id}`);
|
||||
steps.push({
|
||||
name: 'Content Ideas',
|
||||
status: ideas.count > 0 ? 'healthy' : 'warning' as const,
|
||||
message: `${ideas.count} ideas`,
|
||||
});
|
||||
|
||||
// Step 4: Tasks created
|
||||
const tasks = await fetchAPI(`/v1/writer/tasks/?site=${activeSite.id}`);
|
||||
steps.push({
|
||||
name: 'Writer Tasks',
|
||||
status: tasks.count > 0 ? 'healthy' : 'warning' as const,
|
||||
message: `${tasks.count} tasks`,
|
||||
});
|
||||
|
||||
// Step 5: Content generated
|
||||
const content = await fetchAPI(`/v1/writer/content/?site=${activeSite.id}`);
|
||||
steps.push({
|
||||
name: 'Content Generated',
|
||||
status: content.count > 0 ? 'healthy' : 'warning' as const,
|
||||
message: `${content.count} articles`,
|
||||
});
|
||||
|
||||
const hasErrors = steps.some(s => s.status === 'error');
|
||||
const hasWarnings = steps.some(s => s.status === 'warning');
|
||||
|
||||
workflows.push({
|
||||
name: 'Content Generation Pipeline',
|
||||
steps,
|
||||
overall: hasErrors ? 'error' : hasWarnings ? 'warning' : 'healthy',
|
||||
});
|
||||
} catch (error) {
|
||||
workflows.push({
|
||||
name: 'Content Generation Pipeline',
|
||||
steps: [{ name: 'Pipeline Check', status: 'error', message: 'Failed to check workflow' }],
|
||||
overall: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
setWorkflowHealth(workflows);
|
||||
}, [activeSite]);
|
||||
|
||||
// Check integration health
|
||||
const checkIntegrationHealth = useCallback(async () => {
|
||||
if (!activeSite) {
|
||||
setIntegrationHealth(null);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const integrations = await fetchAPI(`/v1/integration/integrations/?site_id=${activeSite.id}`);
|
||||
|
||||
if (integrations.results && integrations.results.length > 0) {
|
||||
const wpIntegration = integrations.results.find((i: any) => i.platform === 'wordpress');
|
||||
|
||||
if (wpIntegration) {
|
||||
const health = await fetchAPI(`/v1/integration/integrations/${wpIntegration.id}/debug-status/`);
|
||||
|
||||
setIntegrationHealth({
|
||||
platform: 'WordPress',
|
||||
connected: wpIntegration.is_active,
|
||||
last_sync: wpIntegration.last_sync_at,
|
||||
sync_enabled: wpIntegration.sync_enabled,
|
||||
plugin_active: health.health?.plugin_active || false,
|
||||
status: wpIntegration.is_active && health.health?.plugin_active ? 'healthy' : 'warning',
|
||||
});
|
||||
} else {
|
||||
setIntegrationHealth(null);
|
||||
}
|
||||
} else {
|
||||
setIntegrationHealth(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to check integration:', error);
|
||||
setIntegrationHealth(null);
|
||||
}
|
||||
}, [activeSite]);
|
||||
|
||||
// Refresh all data
|
||||
const refreshAll = useCallback(async () => {
|
||||
setLoading(true);
|
||||
await Promise.all([
|
||||
fetchSystemMetrics(),
|
||||
fetchApiHealth(),
|
||||
checkWorkflowHealth(),
|
||||
checkIntegrationHealth(),
|
||||
]);
|
||||
setLastUpdate(new Date());
|
||||
setLoading(false);
|
||||
}, [fetchSystemMetrics, fetchApiHealth, checkWorkflowHealth, checkIntegrationHealth]);
|
||||
|
||||
// Initial load and auto-refresh (pause when page not visible)
|
||||
useEffect(() => {
|
||||
let interval: NodeJS.Timeout;
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
// Page not visible - clear interval
|
||||
if (interval) clearInterval(interval);
|
||||
} else {
|
||||
// Page visible - refresh and restart interval
|
||||
refreshAll();
|
||||
interval = setInterval(refreshAll, 30000);
|
||||
}
|
||||
};
|
||||
|
||||
// Initial setup
|
||||
refreshAll();
|
||||
interval = setInterval(refreshAll, 30000);
|
||||
|
||||
// Listen for visibility changes
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
};
|
||||
}, [refreshAll]);
|
||||
|
||||
// Status badge component
|
||||
const StatusBadge = ({ status }: { status: string }) => {
|
||||
const colors = {
|
||||
healthy: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
|
||||
warning: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
|
||||
error: 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400',
|
||||
};
|
||||
|
||||
const icons = {
|
||||
healthy: CheckCircle,
|
||||
warning: AlertTriangle,
|
||||
error: XCircle,
|
||||
};
|
||||
|
||||
const Icon = icons[status as keyof typeof icons] || AlertTriangle;
|
||||
|
||||
return (
|
||||
<div className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium ${colors[status as keyof typeof colors]}`}>
|
||||
<Icon className="h-3 w-3" />
|
||||
{status.charAt(0).toUpperCase() + status.slice(1)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Progress bar component
|
||||
const ProgressBar = ({ value, status }: { value: number; status: string }) => {
|
||||
const colors = {
|
||||
healthy: 'bg-green-500',
|
||||
warning: 'bg-yellow-500',
|
||||
error: 'bg-red-500',
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
className={`h-2 rounded-full transition-all ${colors[status as keyof typeof colors]}`}
|
||||
style={{ width: `${value}%` }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta title="System Status - IGNY8" description="Master status dashboard" />
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-gray-800 dark:text-white/90">System Status</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
Master dashboard showing all system health metrics
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">
|
||||
<Clock className="h-4 w-4 inline mr-1" />
|
||||
Last updated: {lastUpdate.toLocaleTimeString()}
|
||||
</div>
|
||||
<button
|
||||
onClick={refreshAll}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Resources & Services Health */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Server className="h-5 w-5 text-blue-600" />
|
||||
System Resources
|
||||
</h3>
|
||||
<div className="flex gap-6">
|
||||
{/* Compact System Metrics (70% width) */}
|
||||
<div className="flex-1 grid grid-cols-3 gap-4">
|
||||
{/* CPU */}
|
||||
<div className="bg-gray-50 dark:bg-gray-700/30 rounded p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<Cpu className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">CPU</span>
|
||||
</div>
|
||||
<StatusBadge status={systemMetrics?.cpu.status || 'warning'} />
|
||||
</div>
|
||||
<div className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{systemMetrics?.cpu.usage_percent.toFixed(1)}%
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">{systemMetrics?.cpu.cores} cores</p>
|
||||
<ProgressBar value={systemMetrics?.cpu.usage_percent || 0} status={systemMetrics?.cpu.status || 'warning'} />
|
||||
</div>
|
||||
|
||||
{/* Memory */}
|
||||
<div className="bg-gray-50 dark:bg-gray-700/30 rounded p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<MemoryStick className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">Memory</span>
|
||||
</div>
|
||||
<StatusBadge status={systemMetrics?.memory.status || 'warning'} />
|
||||
</div>
|
||||
<div className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{systemMetrics?.memory.usage_percent.toFixed(1)}%
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{systemMetrics?.memory.used_gb.toFixed(1)}/{systemMetrics?.memory.total_gb.toFixed(1)} GB
|
||||
</p>
|
||||
<ProgressBar value={systemMetrics?.memory.usage_percent || 0} status={systemMetrics?.memory.status || 'warning'} />
|
||||
</div>
|
||||
|
||||
{/* Disk */}
|
||||
<div className="bg-gray-50 dark:bg-gray-700/30 rounded p-3">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<HardDrive className="h-4 w-4 text-gray-600 dark:text-gray-400" />
|
||||
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">Disk</span>
|
||||
</div>
|
||||
<StatusBadge status={systemMetrics?.disk.status || 'warning'} />
|
||||
</div>
|
||||
<div className="text-lg font-bold text-gray-900 dark:text-white">
|
||||
{systemMetrics?.disk.usage_percent.toFixed(1)}%
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
||||
{systemMetrics?.disk.used_gb.toFixed(1)}/{systemMetrics?.disk.total_gb.toFixed(1)} GB
|
||||
</p>
|
||||
<ProgressBar value={systemMetrics?.disk.usage_percent || 0} status={systemMetrics?.disk.status || 'warning'} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Services Stack (30% width) */}
|
||||
<div className="w-1/3 space-y-2">
|
||||
<div className="bg-gray-50 dark:bg-gray-700/30 rounded p-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-blue-600" />
|
||||
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">PostgreSQL</span>
|
||||
</div>
|
||||
<StatusBadge status={serviceHealth?.database.status || 'warning'} />
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-700/30 rounded p-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="h-4 w-4 text-red-600" />
|
||||
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">Redis</span>
|
||||
</div>
|
||||
<StatusBadge status={serviceHealth?.redis.status || 'warning'} />
|
||||
</div>
|
||||
|
||||
<div className="bg-gray-50 dark:bg-gray-700/30 rounded p-3 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Activity className="h-4 w-4 text-green-600" />
|
||||
<span className="text-xs font-medium text-gray-700 dark:text-gray-300">Celery</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-xs text-gray-500">{serviceHealth?.celery.worker_count || 0}w</span>
|
||||
<StatusBadge status={serviceHealth?.celery.status || 'warning'} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Site Selector */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 border border-gray-200 dark:border-gray-700">
|
||||
<DebugSiteSelector />
|
||||
</div>
|
||||
|
||||
{/* API Health by Module */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Zap className="h-5 w-5 text-yellow-600" />
|
||||
API Module Health
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{apiHealth.map((group) => (
|
||||
<div key={group.name} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">{group.name}</span>
|
||||
<StatusBadge status={group.status} />
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
|
||||
{group.percentage}%
|
||||
</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 mb-2">
|
||||
{group.healthy}/{group.total} healthy
|
||||
</div>
|
||||
<ProgressBar value={group.percentage} status={group.status} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Workflow Health */}
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Workflow className="h-5 w-5 text-purple-600" />
|
||||
Content Pipeline Status
|
||||
</h3>
|
||||
{workflowHealth.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">Select a site to view workflow health</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{workflowHealth.map((workflow) => (
|
||||
<div key={workflow.name} className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="font-medium text-gray-900 dark:text-white">{workflow.name}</h4>
|
||||
<StatusBadge status={workflow.overall} />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{workflow.steps.map((step, idx) => (
|
||||
<div key={idx} className="flex-1">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-xs text-gray-600 dark:text-gray-400">{step.name}</span>
|
||||
{step.status === 'healthy' ? (
|
||||
<CheckCircle className="h-3 w-3 text-green-600" />
|
||||
) : step.status === 'warning' ? (
|
||||
<AlertTriangle className="h-3 w-3 text-yellow-600" />
|
||||
) : (
|
||||
<XCircle className="h-3 w-3 text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
{step.message && (
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400">{step.message}</p>
|
||||
)}
|
||||
{idx < workflow.steps.length - 1 && (
|
||||
<div className="mt-2 h-1 bg-gray-200 dark:bg-gray-700 rounded">
|
||||
<div className={`h-1 rounded ${
|
||||
step.status === 'healthy' ? 'bg-green-500' :
|
||||
step.status === 'warning' ? 'bg-yellow-500' : 'bg-red-500'
|
||||
}`} style={{ width: '100%' }} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* WordPress Integration */}
|
||||
{integrationHealth && (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-4 flex items-center gap-2">
|
||||
<Globe className="h-5 w-5 text-blue-600" />
|
||||
WordPress Integration
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400 block mb-2">Connection</span>
|
||||
<StatusBadge status={integrationHealth.connected ? 'healthy' : 'error'} />
|
||||
</div>
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400 block mb-2">Plugin Status</span>
|
||||
<StatusBadge status={integrationHealth.plugin_active ? 'healthy' : 'warning'} />
|
||||
</div>
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400 block mb-2">Sync Enabled</span>
|
||||
<StatusBadge status={integrationHealth.sync_enabled ? 'healthy' : 'warning'} />
|
||||
</div>
|
||||
<div className="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<span className="text-sm text-gray-600 dark:text-gray-400 block mb-2">Last Sync</span>
|
||||
<span className="text-sm text-gray-900 dark:text-white">
|
||||
{integrationHealth.last_sync
|
||||
? new Date(integrationHealth.last_sync).toLocaleString()
|
||||
: 'Never'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,192 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import Alert from "../../../components/ui/alert/Alert";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import Button from "../../../components/ui/button/Button";
|
||||
|
||||
export default function Alerts() {
|
||||
const [notifications, setNotifications] = useState<
|
||||
Array<{ id: number; variant: "success" | "error" | "warning" | "info"; title: string; message: string }>
|
||||
>([]);
|
||||
|
||||
const addNotification = (variant: "success" | "error" | "warning" | "info") => {
|
||||
const titles = {
|
||||
success: "Success!",
|
||||
error: "Error Occurred",
|
||||
warning: "Warning",
|
||||
info: "Information",
|
||||
};
|
||||
const messages = {
|
||||
success: "Operation completed successfully.",
|
||||
error: "Something went wrong. Please try again.",
|
||||
warning: "Please review this action carefully.",
|
||||
info: "Here's some useful information for you.",
|
||||
};
|
||||
|
||||
const newNotification = {
|
||||
id: Date.now(),
|
||||
variant,
|
||||
title: titles[variant],
|
||||
message: messages[variant],
|
||||
};
|
||||
|
||||
setNotifications((prev) => [...prev, newNotification]);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
setNotifications((prev) => prev.filter((n) => n.id !== newNotification.id));
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
const removeNotification = (id: number) => {
|
||||
setNotifications((prev) => prev.filter((n) => n.id !== id));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Alerts Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Alerts Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
{/* Interactive Notifications */}
|
||||
<ComponentCard title="Interactive Notifications" desc="Click buttons to add notifications">
|
||||
<div className="flex flex-wrap gap-3 mb-4">
|
||||
<Button onClick={() => addNotification("success")} variant="primary">
|
||||
Add Success
|
||||
</Button>
|
||||
<Button onClick={() => addNotification("error")} variant="primary">
|
||||
Add Error
|
||||
</Button>
|
||||
<Button onClick={() => addNotification("warning")} variant="primary">
|
||||
Add Warning
|
||||
</Button>
|
||||
<Button onClick={() => addNotification("info")} variant="primary">
|
||||
Add Info
|
||||
</Button>
|
||||
{notifications.length > 0 && (
|
||||
<Button onClick={() => setNotifications([])} variant="outline">
|
||||
Clear All
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Notification Stack */}
|
||||
<div className="fixed top-4 right-4 z-50 space-y-2 max-w-md w-full pointer-events-none">
|
||||
{notifications.map((notification) => (
|
||||
<div
|
||||
key={notification.id}
|
||||
className="pointer-events-auto animate-in slide-in-from-top duration-300"
|
||||
>
|
||||
<div className="relative">
|
||||
<Alert
|
||||
variant={notification.variant}
|
||||
title={notification.title}
|
||||
message={notification.message}
|
||||
showLink={false}
|
||||
/>
|
||||
<button
|
||||
onClick={() => removeNotification(notification.id)}
|
||||
className="absolute top-2 right-2 p-1 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
{/* Static Alert Examples */}
|
||||
<ComponentCard title="Success Alert">
|
||||
<div className="space-y-4">
|
||||
<Alert
|
||||
variant="success"
|
||||
title="Success Message"
|
||||
message="Operation completed successfully."
|
||||
showLink={true}
|
||||
linkHref="/"
|
||||
linkText="Learn more"
|
||||
/>
|
||||
<Alert
|
||||
variant="success"
|
||||
title="Success Message"
|
||||
message="Your changes have been saved."
|
||||
showLink={false}
|
||||
/>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Warning Alert">
|
||||
<div className="space-y-4">
|
||||
<Alert
|
||||
variant="warning"
|
||||
title="Warning Message"
|
||||
message="Be cautious when performing this action."
|
||||
showLink={true}
|
||||
linkHref="/"
|
||||
linkText="Learn more"
|
||||
/>
|
||||
<Alert
|
||||
variant="warning"
|
||||
title="Warning Message"
|
||||
message="This action cannot be undone."
|
||||
showLink={false}
|
||||
/>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Error Alert">
|
||||
<div className="space-y-4">
|
||||
<Alert
|
||||
variant="error"
|
||||
title="Error Message"
|
||||
message="Something went wrong. Please try again."
|
||||
showLink={true}
|
||||
linkHref="/"
|
||||
linkText="Learn more"
|
||||
/>
|
||||
<Alert
|
||||
variant="error"
|
||||
title="Error Message"
|
||||
message="Failed to save changes. Please check your connection."
|
||||
showLink={false}
|
||||
/>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Info Alert">
|
||||
<div className="space-y-4">
|
||||
<Alert
|
||||
variant="info"
|
||||
title="Info Message"
|
||||
message="Here's some useful information for you."
|
||||
showLink={true}
|
||||
linkHref="/"
|
||||
linkText="Learn more"
|
||||
/>
|
||||
<Alert
|
||||
variant="info"
|
||||
title="Info Message"
|
||||
message="New features are available. Check them out!"
|
||||
showLink={false}
|
||||
/>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import Avatar from "../../../components/ui/avatar/Avatar";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
|
||||
export default function Avatars() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Avatars Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Avatars Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Default Avatar">
|
||||
{/* Default Avatar (No Status) */}
|
||||
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
|
||||
<Avatar src="/images/user/user-01.jpg" size="xsmall" />
|
||||
<Avatar src="/images/user/user-01.jpg" size="small" />
|
||||
<Avatar src="/images/user/user-01.jpg" size="medium" />
|
||||
<Avatar src="/images/user/user-01.jpg" size="large" />
|
||||
<Avatar src="/images/user/user-01.jpg" size="xlarge" />
|
||||
<Avatar src="/images/user/user-01.jpg" size="xxlarge" />
|
||||
</div>
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Avatar with online indicator">
|
||||
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xsmall"
|
||||
status="online"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="small"
|
||||
status="online"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="medium"
|
||||
status="online"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="large"
|
||||
status="online"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xlarge"
|
||||
status="online"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xxlarge"
|
||||
status="online"
|
||||
/>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Avatar with Offline indicator">
|
||||
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xsmall"
|
||||
status="offline"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="small"
|
||||
status="offline"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="medium"
|
||||
status="offline"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="large"
|
||||
status="offline"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xlarge"
|
||||
status="offline"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xxlarge"
|
||||
status="offline"
|
||||
/>
|
||||
</div>
|
||||
</ComponentCard>{" "}
|
||||
<ComponentCard title="Avatar with busy indicator">
|
||||
<div className="flex flex-col items-center justify-center gap-5 sm:flex-row">
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xsmall"
|
||||
status="busy"
|
||||
/>
|
||||
<Avatar src="/images/user/user-01.jpg" size="small" status="busy" />
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="medium"
|
||||
status="busy"
|
||||
/>
|
||||
<Avatar src="/images/user/user-01.jpg" size="large" status="busy" />
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xlarge"
|
||||
status="busy"
|
||||
/>
|
||||
<Avatar
|
||||
src="/images/user/user-01.jpg"
|
||||
size="xxlarge"
|
||||
status="busy"
|
||||
/>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
import Badge from "../../../components/ui/badge/Badge";
|
||||
import { PlusIcon } from "../../../icons";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
|
||||
export default function Badges() {
|
||||
return (
|
||||
<div>
|
||||
<PageMeta
|
||||
title="React.js Badges Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Badges Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="With Light Background">
|
||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
||||
{/* Light Variant */}
|
||||
<Badge variant="light" color="primary">
|
||||
Primary
|
||||
</Badge>
|
||||
<Badge variant="light" color="success">
|
||||
Success
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="error">
|
||||
Error
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="warning">
|
||||
Warning
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="info">
|
||||
Info
|
||||
</Badge>
|
||||
<Badge variant="light" color="light">
|
||||
Light
|
||||
</Badge>
|
||||
<Badge variant="light" color="dark">
|
||||
Dark
|
||||
</Badge>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
<ComponentCard title="With Solid Background">
|
||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
||||
{/* Light Variant */}
|
||||
<Badge variant="solid" color="primary">
|
||||
Primary
|
||||
</Badge>
|
||||
<Badge variant="solid" color="success">
|
||||
Success
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="error">
|
||||
Error
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="warning">
|
||||
Warning
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="info">
|
||||
Info
|
||||
</Badge>
|
||||
<Badge variant="solid" color="light">
|
||||
Light
|
||||
</Badge>
|
||||
<Badge variant="solid" color="dark">
|
||||
Dark
|
||||
</Badge>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Light Background with Left Icon">
|
||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
||||
<Badge variant="light" color="primary" startIcon={<PlusIcon />}>
|
||||
Primary
|
||||
</Badge>
|
||||
<Badge variant="light" color="success" startIcon={<PlusIcon />}>
|
||||
Success
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="error" startIcon={<PlusIcon />}>
|
||||
Error
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="warning" startIcon={<PlusIcon />}>
|
||||
Warning
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="info" startIcon={<PlusIcon />}>
|
||||
Info
|
||||
</Badge>
|
||||
<Badge variant="light" color="light" startIcon={<PlusIcon />}>
|
||||
Light
|
||||
</Badge>
|
||||
<Badge variant="light" color="dark" startIcon={<PlusIcon />}>
|
||||
Dark
|
||||
</Badge>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Solid Background with Left Icon">
|
||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
||||
<Badge variant="solid" color="primary" startIcon={<PlusIcon />}>
|
||||
Primary
|
||||
</Badge>
|
||||
<Badge variant="solid" color="success" startIcon={<PlusIcon />}>
|
||||
Success
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="error" startIcon={<PlusIcon />}>
|
||||
Error
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="warning" startIcon={<PlusIcon />}>
|
||||
Warning
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="info" startIcon={<PlusIcon />}>
|
||||
Info
|
||||
</Badge>
|
||||
<Badge variant="solid" color="light" startIcon={<PlusIcon />}>
|
||||
Light
|
||||
</Badge>
|
||||
<Badge variant="solid" color="dark" startIcon={<PlusIcon />}>
|
||||
Dark
|
||||
</Badge>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Light Background with Right Icon">
|
||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
||||
<Badge variant="light" color="primary" endIcon={<PlusIcon />}>
|
||||
Primary
|
||||
</Badge>
|
||||
<Badge variant="light" color="success" endIcon={<PlusIcon />}>
|
||||
Success
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="error" endIcon={<PlusIcon />}>
|
||||
Error
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="warning" endIcon={<PlusIcon />}>
|
||||
Warning
|
||||
</Badge>{" "}
|
||||
<Badge variant="light" color="info" endIcon={<PlusIcon />}>
|
||||
Info
|
||||
</Badge>
|
||||
<Badge variant="light" color="light" endIcon={<PlusIcon />}>
|
||||
Light
|
||||
</Badge>
|
||||
<Badge variant="light" color="dark" endIcon={<PlusIcon />}>
|
||||
Dark
|
||||
</Badge>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Solid Background with Right Icon">
|
||||
<div className="flex flex-wrap gap-4 sm:items-center sm:justify-center">
|
||||
<Badge variant="solid" color="primary" endIcon={<PlusIcon />}>
|
||||
Primary
|
||||
</Badge>
|
||||
<Badge variant="solid" color="success" endIcon={<PlusIcon />}>
|
||||
Success
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="error" endIcon={<PlusIcon />}>
|
||||
Error
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="warning" endIcon={<PlusIcon />}>
|
||||
Warning
|
||||
</Badge>{" "}
|
||||
<Badge variant="solid" color="info" endIcon={<PlusIcon />}>
|
||||
Info
|
||||
</Badge>
|
||||
<Badge variant="solid" color="light" endIcon={<PlusIcon />}>
|
||||
Light
|
||||
</Badge>
|
||||
<Badge variant="solid" color="dark" endIcon={<PlusIcon />}>
|
||||
Dark
|
||||
</Badge>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import { Breadcrumb } from "../../../components/ui/breadcrumb";
|
||||
|
||||
export default function BreadcrumbPage() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Breadcrumb Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Breadcrumb Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Default Breadcrumb">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{ label: "Home", path: "/" },
|
||||
{ label: "UI Elements", path: "/ui-elements" },
|
||||
{ label: "Breadcrumb" },
|
||||
]}
|
||||
/>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Breadcrumb with Icon">
|
||||
<Breadcrumb
|
||||
items={[
|
||||
{
|
||||
label: "Home",
|
||||
path: "/",
|
||||
icon: (
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{ label: "UI Elements", path: "/ui-elements" },
|
||||
{ label: "Breadcrumb" },
|
||||
]}
|
||||
/>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import Button from "../../../components/ui/button/Button";
|
||||
import { BoxIcon } from "../../../icons";
|
||||
|
||||
export default function Buttons() {
|
||||
return (
|
||||
<div>
|
||||
<PageMeta
|
||||
title="React.js Buttons Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Buttons Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
{/* Primary Button */}
|
||||
<ComponentCard title="Primary Button">
|
||||
<div className="flex items-center gap-5">
|
||||
<Button size="sm" variant="primary">
|
||||
Button Text
|
||||
</Button>
|
||||
<Button size="md" variant="primary">
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
{/* Primary Button with Start Icon */}
|
||||
<ComponentCard title="Primary Button with Left Icon">
|
||||
<div className="flex items-center gap-5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
startIcon={<BoxIcon className="size-5" />}
|
||||
>
|
||||
Button Text
|
||||
</Button>
|
||||
<Button
|
||||
size="md"
|
||||
variant="primary"
|
||||
startIcon={<BoxIcon className="size-5" />}
|
||||
>
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
{/* Primary Button with Start Icon */}
|
||||
<ComponentCard title="Primary Button with Right Icon">
|
||||
<div className="flex items-center gap-5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
endIcon={<BoxIcon className="size-5" />}
|
||||
>
|
||||
Button Text
|
||||
</Button>
|
||||
<Button
|
||||
size="md"
|
||||
variant="primary"
|
||||
endIcon={<BoxIcon className="size-5" />}
|
||||
>
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
{/* Outline Button */}
|
||||
<ComponentCard title="Secondary Button">
|
||||
<div className="flex items-center gap-5">
|
||||
{/* Outline Button */}
|
||||
<Button size="sm" variant="outline">
|
||||
Button Text
|
||||
</Button>
|
||||
<Button size="md" variant="outline">
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
{/* Outline Button with Start Icon */}
|
||||
<ComponentCard title="Outline Button with Left Icon">
|
||||
<div className="flex items-center gap-5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
startIcon={<BoxIcon className="size-5" />}
|
||||
>
|
||||
Button Text
|
||||
</Button>
|
||||
<Button
|
||||
size="md"
|
||||
variant="outline"
|
||||
startIcon={<BoxIcon className="size-5" />}
|
||||
>
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>{" "}
|
||||
{/* Outline Button with Start Icon */}
|
||||
<ComponentCard title="Outline Button with Right Icon">
|
||||
<div className="flex items-center gap-5">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
endIcon={<BoxIcon className="size-5" />}
|
||||
>
|
||||
Button Text
|
||||
</Button>
|
||||
<Button
|
||||
size="md"
|
||||
variant="outline"
|
||||
endIcon={<BoxIcon className="size-5" />}
|
||||
>
|
||||
Button Text
|
||||
</Button>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import { ButtonGroup, ButtonGroupItem } from "../../../components/ui/button-group";
|
||||
|
||||
export default function ButtonsGroup() {
|
||||
const [activeGroup, setActiveGroup] = useState("left");
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Button Groups Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Button Groups Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Default Button Group">
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem
|
||||
isActive={activeGroup === "left"}
|
||||
onClick={() => setActiveGroup("left")}
|
||||
className="rounded-l-lg border-l-0"
|
||||
>
|
||||
Left
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem
|
||||
isActive={activeGroup === "center"}
|
||||
onClick={() => setActiveGroup("center")}
|
||||
className="border-l border-r border-gray-300 dark:border-gray-700"
|
||||
>
|
||||
Center
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem
|
||||
isActive={activeGroup === "right"}
|
||||
onClick={() => setActiveGroup("right")}
|
||||
className="rounded-r-lg border-r-0"
|
||||
>
|
||||
Right
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Icon Button Group">
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem className="rounded-l-lg border-l-0">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem className="border-l border-r border-gray-300 dark:border-gray-700">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 3a1 1 0 011 1v12a1 1 0 11-2 0V4a1 1 0 011-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem className="rounded-r-lg border-r-0">
|
||||
<svg
|
||||
className="w-5 h-5"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M3 10a1 1 0 011 1h12a1 1 0 110-2H4a1 1 0 01-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import {
|
||||
Card,
|
||||
CardTitle,
|
||||
CardDescription,
|
||||
CardAction,
|
||||
CardIcon,
|
||||
} from "../../../components/ui/card/Card";
|
||||
|
||||
export default function Cards() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Cards Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Cards Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Basic Card">
|
||||
<Card>
|
||||
<CardTitle>Card Title</CardTitle>
|
||||
<CardDescription>
|
||||
This is a basic card with title and description.
|
||||
</CardDescription>
|
||||
</Card>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Card with Icon">
|
||||
<Card>
|
||||
<CardIcon>
|
||||
<svg
|
||||
className="w-6 h-6"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M9 2a1 1 0 000 2h2a1 1 0 100-2H9z" />
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</CardIcon>
|
||||
<CardTitle>Card with Icon</CardTitle>
|
||||
<CardDescription>This card includes an icon at the top.</CardDescription>
|
||||
<CardAction>Learn More</CardAction>
|
||||
</Card>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Card with Image">
|
||||
<Card>
|
||||
<img
|
||||
src="https://via.placeholder.com/400x200"
|
||||
alt="Card"
|
||||
className="w-full h-48 object-cover rounded-t-xl"
|
||||
/>
|
||||
<CardTitle>Card with Image</CardTitle>
|
||||
<CardDescription>
|
||||
This card includes an image at the top.
|
||||
</CardDescription>
|
||||
</Card>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
|
||||
export default function Carousel() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Carousel Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Carousel Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Carousel">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Carousel component will be implemented here.
|
||||
</p>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import { Dropdown } from "../../../components/ui/dropdown/Dropdown";
|
||||
import { DropdownItem } from "../../../components/ui/dropdown/DropdownItem";
|
||||
import Button from "../../../components/ui/button/Button";
|
||||
|
||||
export default function Dropdowns() {
|
||||
const [dropdown1, setDropdown1] = useState(false);
|
||||
const [dropdown2, setDropdown2] = useState(false);
|
||||
const [dropdown3, setDropdown3] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Dropdowns Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Dropdowns Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Default Dropdown">
|
||||
<div className="relative inline-block">
|
||||
<Button onClick={() => setDropdown1(!dropdown1)}>
|
||||
Dropdown Default
|
||||
</Button>
|
||||
<Dropdown
|
||||
isOpen={dropdown1}
|
||||
onClose={() => setDropdown1(false)}
|
||||
className="w-48 p-2 mt-2"
|
||||
>
|
||||
<DropdownItem
|
||||
onItemClick={() => setDropdown1(false)}
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
Edit
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
onItemClick={() => setDropdown1(false)}
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Dropdown with Divider">
|
||||
<div className="relative inline-block">
|
||||
<Button onClick={() => setDropdown2(!dropdown2)}>
|
||||
Dropdown with Divider
|
||||
</Button>
|
||||
<Dropdown
|
||||
isOpen={dropdown2}
|
||||
onClose={() => setDropdown2(false)}
|
||||
className="w-48 p-2 mt-2"
|
||||
>
|
||||
<DropdownItem
|
||||
onItemClick={() => setDropdown2(false)}
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
Edit
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
onItemClick={() => setDropdown2(false)}
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
View
|
||||
</DropdownItem>
|
||||
<div className="my-2 border-t border-gray-200 dark:border-gray-800"></div>
|
||||
<DropdownItem
|
||||
onItemClick={() => setDropdown2(false)}
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-red-600 rounded-lg text-theme-sm hover:bg-red-50 hover:text-red-700 dark:text-red-400 dark:hover:bg-red-900/20 dark:hover:text-red-300"
|
||||
>
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Dropdown with Icon">
|
||||
<div className="relative inline-block">
|
||||
<Button onClick={() => setDropdown3(!dropdown3)}>
|
||||
Dropdown with Icon
|
||||
</Button>
|
||||
<Dropdown
|
||||
isOpen={dropdown3}
|
||||
onClose={() => setDropdown3(false)}
|
||||
className="w-48 p-2 mt-2"
|
||||
>
|
||||
<DropdownItem
|
||||
onItemClick={() => setDropdown3(false)}
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" />
|
||||
</svg>
|
||||
Edit
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
onItemClick={() => setDropdown3(false)}
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-gray-700 rounded-lg text-theme-sm hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
View
|
||||
</DropdownItem>
|
||||
<div className="my-2 border-t border-gray-200 dark:border-gray-800"></div>
|
||||
<DropdownItem
|
||||
onItemClick={() => setDropdown3(false)}
|
||||
className="flex items-center gap-3 px-3 py-2 font-medium text-red-600 rounded-lg text-theme-sm hover:bg-red-50 hover:text-red-700 dark:text-red-400 dark:hover:bg-red-900/20 dark:hover:text-red-300"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
Delete
|
||||
</DropdownItem>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import ResponsiveImage from "../../../components/ui/images/ResponsiveImage";
|
||||
import TwoColumnImageGrid from "../../../components/ui/images/TwoColumnImageGrid";
|
||||
import ThreeColumnImageGrid from "../../../components/ui/images/ThreeColumnImageGrid";
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
|
||||
export default function Images() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Images Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Images page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Responsive image">
|
||||
<ResponsiveImage />
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Image in 2 Grid">
|
||||
<TwoColumnImageGrid />
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Image in 3 Grid">
|
||||
<ThreeColumnImageGrid />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
|
||||
export default function Links() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Links Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Links Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Links">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<a
|
||||
href="#"
|
||||
className="text-brand-500 hover:text-brand-600 underline"
|
||||
>
|
||||
Primary Link
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
href="#"
|
||||
className="text-gray-700 dark:text-gray-300 hover:text-brand-500 underline"
|
||||
>
|
||||
Default Link
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import { List, ListItem } from "../../../components/ui/list";
|
||||
|
||||
export default function ListPage() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js List Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js List Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Unordered List">
|
||||
<List variant="unordered">
|
||||
<ListItem>Item 1</ListItem>
|
||||
<ListItem>Item 2</ListItem>
|
||||
<ListItem>Item 3</ListItem>
|
||||
</List>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Ordered List">
|
||||
<List variant="ordered">
|
||||
<ListItem>First Item</ListItem>
|
||||
<ListItem>Second Item</ListItem>
|
||||
<ListItem>Third Item</ListItem>
|
||||
</List>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Button List">
|
||||
<List variant="button">
|
||||
<ListItem variant="button" onClick={() => alert("Clicked Item 1")}>
|
||||
Button Item 1
|
||||
</ListItem>
|
||||
<ListItem variant="button" onClick={() => alert("Clicked Item 2")}>
|
||||
Button Item 2
|
||||
</ListItem>
|
||||
<ListItem variant="button" onClick={() => alert("Clicked Item 3")}>
|
||||
Button Item 3
|
||||
</ListItem>
|
||||
</List>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import { Modal } from "../../../components/ui/modal";
|
||||
import Button from "../../../components/ui/button/Button";
|
||||
import ConfirmDialog from "../../../components/common/ConfirmDialog";
|
||||
import AlertModal from "../../../components/ui/alert/AlertModal";
|
||||
|
||||
export default function Modals() {
|
||||
const [isDefaultModalOpen, setIsDefaultModalOpen] = useState(false);
|
||||
const [isCenteredModalOpen, setIsCenteredModalOpen] = useState(false);
|
||||
const [isFormModalOpen, setIsFormModalOpen] = useState(false);
|
||||
const [isFullScreenModalOpen, setIsFullScreenModalOpen] = useState(false);
|
||||
const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false);
|
||||
const [isSuccessAlertOpen, setIsSuccessAlertOpen] = useState(false);
|
||||
const [isInfoAlertOpen, setIsInfoAlertOpen] = useState(false);
|
||||
const [isWarningAlertOpen, setIsWarningAlertOpen] = useState(false);
|
||||
const [isDangerAlertOpen, setIsDangerAlertOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Modals Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Modals Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Default Modal">
|
||||
<Button onClick={() => setIsDefaultModalOpen(true)}>
|
||||
Open Default Modal
|
||||
</Button>
|
||||
<Modal
|
||||
isOpen={isDefaultModalOpen}
|
||||
onClose={() => setIsDefaultModalOpen(false)}
|
||||
className="max-w-lg"
|
||||
>
|
||||
<div className="p-6">
|
||||
<h2 className="text-xl font-bold mb-4">Default Modal Title</h2>
|
||||
<p>This is a default modal. It can contain any content.</p>
|
||||
<div className="flex justify-end gap-4 mt-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setIsDefaultModalOpen(false)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button variant="primary">Save Changes</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Centered Modal">
|
||||
<Button onClick={() => setIsCenteredModalOpen(true)}>
|
||||
Open Centered Modal
|
||||
</Button>
|
||||
<Modal
|
||||
isOpen={isCenteredModalOpen}
|
||||
onClose={() => setIsCenteredModalOpen(false)}
|
||||
className="max-w-md"
|
||||
>
|
||||
<div className="p-6 text-center">
|
||||
<h2 className="text-xl font-bold mb-4">Centered Modal Title</h2>
|
||||
<p>This modal is vertically and horizontally centered.</p>
|
||||
<Button
|
||||
onClick={() => setIsCenteredModalOpen(false)}
|
||||
className="mt-6"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Full Screen Modal">
|
||||
<Button onClick={() => setIsFullScreenModalOpen(true)}>
|
||||
Open Full Screen Modal
|
||||
</Button>
|
||||
<Modal
|
||||
isOpen={isFullScreenModalOpen}
|
||||
onClose={() => setIsFullScreenModalOpen(false)}
|
||||
isFullscreen={true}
|
||||
>
|
||||
<div className="p-6 bg-white dark:bg-gray-900 w-full h-full flex flex-col">
|
||||
<h2 className="text-2xl font-bold mb-4">Full Screen Modal</h2>
|
||||
<p className="flex-grow">
|
||||
This modal takes up the entire screen. Useful for complex forms
|
||||
or detailed views.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => setIsFullScreenModalOpen(false)}
|
||||
className="mt-6 self-end"
|
||||
>
|
||||
Close Full Screen
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Confirmation Dialog">
|
||||
<Button
|
||||
onClick={() => setIsConfirmDialogOpen(true)}
|
||||
variant="danger"
|
||||
>
|
||||
Open Confirmation Dialog
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
isOpen={isConfirmDialogOpen}
|
||||
onClose={() => setIsConfirmDialogOpen(false)}
|
||||
onConfirm={() => {
|
||||
alert("Action Confirmed!");
|
||||
setIsConfirmDialogOpen(false);
|
||||
}}
|
||||
title="Confirm Action"
|
||||
message="Are you sure you want to proceed with this action? It cannot be undone."
|
||||
confirmText="Proceed"
|
||||
variant="danger"
|
||||
/>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Alert Modals">
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Button
|
||||
onClick={() => setIsSuccessAlertOpen(true)}
|
||||
variant="success"
|
||||
>
|
||||
Success Alert
|
||||
</Button>
|
||||
<Button onClick={() => setIsInfoAlertOpen(true)} variant="info">
|
||||
Info Alert
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsWarningAlertOpen(true)}
|
||||
variant="warning"
|
||||
>
|
||||
Warning Alert
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setIsDangerAlertOpen(true)}
|
||||
variant="danger"
|
||||
>
|
||||
Danger Alert
|
||||
</Button>
|
||||
</div>
|
||||
<AlertModal
|
||||
isOpen={isSuccessAlertOpen}
|
||||
onClose={() => setIsSuccessAlertOpen(false)}
|
||||
title="Success!"
|
||||
message="Your operation was completed successfully."
|
||||
variant="success"
|
||||
/>
|
||||
<AlertModal
|
||||
isOpen={isInfoAlertOpen}
|
||||
onClose={() => setIsInfoAlertOpen(false)}
|
||||
title="Information"
|
||||
message="This is an informational message for the user."
|
||||
variant="info"
|
||||
/>
|
||||
<AlertModal
|
||||
isOpen={isWarningAlertOpen}
|
||||
onClose={() => setIsWarningAlertOpen(false)}
|
||||
title="Warning!"
|
||||
message="Please be careful, this action has consequences."
|
||||
variant="warning"
|
||||
/>
|
||||
<AlertModal
|
||||
isOpen={isDangerAlertOpen}
|
||||
onClose={() => setIsDangerAlertOpen(false)}
|
||||
title="Danger!"
|
||||
message="This is a critical alert. Proceed with caution."
|
||||
variant="danger"
|
||||
/>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,278 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import Alert from '../../../components/ui/alert/Alert';
|
||||
import { useToast } from '../../../components/ui/toast/ToastContainer';
|
||||
import PageMeta from '../../../components/common/PageMeta';
|
||||
|
||||
export default function Notifications() {
|
||||
const toast = useToast();
|
||||
|
||||
// State for inline notifications (for demo purposes)
|
||||
const [showSuccess, setShowSuccess] = useState(true);
|
||||
const [showInfo, setShowInfo] = useState(true);
|
||||
const [showWarning, setShowWarning] = useState(true);
|
||||
const [showError, setShowError] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Notifications Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Notifications Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
{/* Components Grid */}
|
||||
<div className="grid grid-cols-1 gap-5 xl:grid-cols-2 xl:gap-6">
|
||||
{/* Announcement Bar Card */}
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03] xl:col-span-2">
|
||||
<div className="px-6 py-5">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
Announcement Bar
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-100 dark:border-gray-800 sm:p-6">
|
||||
<div className="flex items-center justify-between gap-4 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Lightning bolt icon */}
|
||||
<div className="flex-shrink-0 w-10 h-10 bg-blue-light-100 dark:bg-blue-light-500/20 rounded-lg flex items-center justify-center">
|
||||
<svg
|
||||
className="w-5 h-5 text-blue-light-500"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M11.3 1.046A1 1 0 0112 2v5h4a1 1 0 01.82 1.573l-7 10A1 1 0 018 18v-5H4a1 1 0 01-.82-1.573l7-10a1 1 0 011.12-.38z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-gray-800 dark:text-white">
|
||||
New update! Available
|
||||
</p>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Enjoy improved functionality and enhancements.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<button className="px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 transition-colors">
|
||||
Later
|
||||
</button>
|
||||
<button className="px-4 py-2 text-sm font-medium text-white bg-brand-500 rounded-lg hover:bg-brand-600 transition-colors">
|
||||
Update Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toast Notification Card */}
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03] xl:col-span-2">
|
||||
<div className="px-6 py-5">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
Toast Notification
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-100 dark:border-gray-800 sm:p-6">
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<button
|
||||
onClick={() => toast.success('Success! Action Completed!', 'Your action has been completed successfully.')}
|
||||
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-success-500 hover:bg-success-600 transition-colors"
|
||||
>
|
||||
Success Toast
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toast.info('Heads Up! New Information', 'This is an informational message.')}
|
||||
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-blue-light-500 hover:bg-blue-light-600 transition-colors"
|
||||
>
|
||||
Info Toast
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toast.warning('Alert: Double Check Required', 'Please review this action carefully.')}
|
||||
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-warning-500 hover:bg-warning-600 transition-colors"
|
||||
>
|
||||
Warning Toast
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toast.error('Something Went Wrong', 'An error occurred. Please try again.')}
|
||||
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-error-500 hover:bg-error-600 transition-colors"
|
||||
>
|
||||
Error Toast
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Toast notifications appear in the top right corner with margin from top. They have a thin light gray border around the entire perimeter.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Success Notification Card */}
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="px-6 py-5">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
Success Notification
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-100 dark:border-gray-800 sm:p-6">
|
||||
{showSuccess && (
|
||||
<div className="relative">
|
||||
<Alert
|
||||
variant="success"
|
||||
title="Success! Action Completed!"
|
||||
message="Your action has been completed successfully."
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowSuccess(false)}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!showSuccess && (
|
||||
<button
|
||||
onClick={() => setShowSuccess(true)}
|
||||
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-success-500 hover:bg-success-600 transition-colors"
|
||||
>
|
||||
Show Success Notification
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Notification Card */}
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="px-6 py-5">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
Info Notification
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-100 dark:border-gray-800 sm:p-6">
|
||||
{showInfo && (
|
||||
<div className="relative">
|
||||
<Alert
|
||||
variant="info"
|
||||
title="Heads Up! New Information"
|
||||
message="This is an informational message for your attention."
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowInfo(false)}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!showInfo && (
|
||||
<button
|
||||
onClick={() => setShowInfo(true)}
|
||||
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-blue-light-500 hover:bg-blue-light-600 transition-colors"
|
||||
>
|
||||
Show Info Notification
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Warning Notification Card */}
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="px-6 py-5">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
Warning Notification
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-100 dark:border-gray-800 sm:p-6">
|
||||
{showWarning && (
|
||||
<div className="relative">
|
||||
<Alert
|
||||
variant="warning"
|
||||
title="Alert: Double Check Required"
|
||||
message="Please review this action carefully before proceeding."
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowWarning(false)}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!showWarning && (
|
||||
<button
|
||||
onClick={() => setShowWarning(true)}
|
||||
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-warning-500 hover:bg-warning-600 transition-colors"
|
||||
>
|
||||
Show Warning Notification
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error Notification Card */}
|
||||
<div className="rounded-2xl border border-gray-200 bg-white dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="px-6 py-5">
|
||||
<h3 className="text-base font-medium text-gray-800 dark:text-white/90">
|
||||
Error Notification
|
||||
</h3>
|
||||
</div>
|
||||
<div className="p-4 border-t border-gray-100 dark:border-gray-800 sm:p-6">
|
||||
{showError && (
|
||||
<div className="relative">
|
||||
<Alert
|
||||
variant="error"
|
||||
title="Something Went Wrong"
|
||||
message="An error occurred. Please try again or contact support."
|
||||
/>
|
||||
<button
|
||||
onClick={() => setShowError(false)}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors"
|
||||
aria-label="Close"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{!showError && (
|
||||
<button
|
||||
onClick={() => setShowError(true)}
|
||||
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-error-500 hover:bg-error-600 transition-colors"
|
||||
>
|
||||
Show Error Notification
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import { Pagination } from "../../../components/ui/pagination/Pagination";
|
||||
|
||||
export default function PaginationPage() {
|
||||
const [page1, setPage1] = useState(1);
|
||||
const [page2, setPage2] = useState(1);
|
||||
const [page3, setPage3] = useState(1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Pagination Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Pagination Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Pagination with Text">
|
||||
<Pagination
|
||||
currentPage={page1}
|
||||
totalPages={10}
|
||||
onPageChange={setPage1}
|
||||
variant="text"
|
||||
/>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Pagination with Text and Icon">
|
||||
<Pagination
|
||||
currentPage={page2}
|
||||
totalPages={10}
|
||||
onPageChange={setPage2}
|
||||
variant="text-icon"
|
||||
/>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Pagination with Icon">
|
||||
<Pagination
|
||||
currentPage={page3}
|
||||
totalPages={10}
|
||||
onPageChange={setPage3}
|
||||
variant="icon"
|
||||
/>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
|
||||
export default function Popovers() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Popovers Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Popovers Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Popovers">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Popover component will be implemented here.
|
||||
</p>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import { PricingTable, PricingPlan } from "../../../components/ui/pricing-table";
|
||||
import PricingTable1 from "../../../components/ui/pricing-table/pricing-table-1";
|
||||
import { getPublicPlans } from "../../../services/billing.api";
|
||||
|
||||
interface Plan {
|
||||
id: number;
|
||||
name: string;
|
||||
slug?: string;
|
||||
price: number | string;
|
||||
original_price?: number;
|
||||
annual_discount_percent?: number;
|
||||
is_featured?: boolean;
|
||||
max_sites?: number;
|
||||
max_users?: number;
|
||||
max_keywords?: number;
|
||||
max_clusters?: number;
|
||||
max_content_ideas?: number;
|
||||
max_content_words?: number;
|
||||
max_images_basic?: number;
|
||||
max_images_premium?: number;
|
||||
max_image_prompts?: number;
|
||||
included_credits?: number;
|
||||
}
|
||||
|
||||
// Sample icons for variant 2
|
||||
const PersonIcon = () => (
|
||||
<svg className="fill-current" width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M11.4072 8.64984C11.4072 6.77971 12.9232 5.26367 14.7934 5.26367C16.6635 5.26367 18.1795 6.77971 18.1795 8.64984C18.1795 10.52 16.6635 12.036 14.7934 12.036C12.9232 12.036 11.4072 10.52 11.4072 8.64984ZM14.7934 3.48633C11.9416 3.48633 9.62986 5.79811 9.62986 8.64984C9.62986 11.5016 11.9416 13.8133 14.7934 13.8133C17.6451 13.8133 19.9569 11.5016 19.9569 8.64984C19.9569 5.79811 17.6451 3.48633 14.7934 3.48633ZM12.8251 15.6037C8.49586 15.6037 4.98632 19.1133 4.98632 23.4425V23.847C4.98632 24.3378 5.38419 24.7357 5.87499 24.7357C6.36579 24.7357 6.76366 24.3378 6.76366 23.847V23.4425C6.76366 20.0949 9.47746 17.3811 12.8251 17.3811H16.7635C20.1111 17.3811 22.8249 20.0949 22.8249 23.4425V23.847C22.8249 24.3378 23.2228 24.7357 23.7136 24.7357C24.2044 24.7357 24.6023 24.3378 24.6023 23.847V23.4425C24.6023 19.1133 21.0927 15.6037 16.7635 15.6037H12.8251Z" fill=""></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const BriefcaseIcon = () => (
|
||||
<svg className="fill-current" width="29" height="28" viewBox="0 0 29 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M12.2969 3.55469C10.8245 3.55469 9.6309 4.7483 9.6309 6.2207V7.10938H6.29462C4.82222 7.10938 3.6286 8.30299 3.6286 9.77539V20.4395C3.6286 21.9119 4.82222 23.1055 6.29462 23.1055H23.4758C24.9482 23.1055 26.1419 21.9119 26.1419 20.4395V9.77539C26.1419 8.30299 24.9482 7.10938 23.4758 7.10938H19.7025V6.2207C19.7025 4.7483 18.5089 3.55469 17.0365 3.55469H12.2969ZM18.8148 8.88672C18.8145 8.88672 18.8142 8.88672 18.8138 8.88672H10.5196C10.5193 8.88672 10.5189 8.88672 10.5186 8.88672H6.29462C5.80382 8.88672 5.40595 9.28459 5.40595 9.77539V10.9666L14.5355 14.8792C14.759 14.975 15.012 14.975 15.2356 14.8792L24.3645 10.9669V9.77539C24.3645 9.28459 23.9666 8.88672 23.4758 8.88672H18.8148ZM17.9252 7.10938V6.2207C17.9252 5.7299 17.5273 5.33203 17.0365 5.33203H12.2969C11.8061 5.33203 11.4082 5.7299 11.4082 6.2207V7.10938H17.9252ZM5.40595 20.4395V12.9003L13.8353 16.5129C14.506 16.8003 15.2651 16.8003 15.9357 16.5129L24.3645 12.9006V20.4395C24.3645 20.9303 23.9666 21.3281 23.4758 21.3281H6.29462C5.80382 21.3281 5.40595 20.9303 5.40595 20.4395Z" fill=""></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const StarIcon = () => (
|
||||
<svg className="fill-current" width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fillRule="evenodd" clipRule="evenodd" d="M23.7507 1.28757C24.0978 0.940553 24.6605 0.940611 25.0075 1.28769C25.3545 1.63478 25.3544 2.19745 25.0074 2.54447L19.8787 7.67208C19.5316 8.0191 18.9689 8.01904 18.6219 7.67195C18.2749 7.32487 18.275 6.76219 18.622 6.41518L23.7507 1.28757ZM19.4452 3.1553C19.7922 2.80822 19.7921 2.24554 19.4451 1.89853C19.098 1.55151 18.5353 1.55157 18.1883 1.89866L16.4386 3.64866C16.0916 3.99574 16.0917 4.55842 16.4388 4.90543C16.7859 5.25244 17.3485 5.25238 17.6955 4.9053L19.4452 3.1553ZM13.8188 4.02442C13.6691 3.72109 13.3602 3.52905 13.0219 3.52905C12.6837 3.52905 12.3747 3.72109 12.225 4.02442L9.39921 9.75015L3.08049 10.6683C2.74574 10.717 2.46763 10.9514 2.3631 11.2731C2.25857 11.5948 2.34575 11.948 2.58797 12.1841L7.16024 16.641L6.08087 22.9342C6.02369 23.2676 6.16075 23.6045 6.43441 23.8033C6.70807 24.0022 7.07088 24.0284 7.37029 23.871L13.0219 20.8997L18.6736 23.871C18.973 24.0284 19.3358 24.0022 19.6094 23.8033C19.8831 23.6045 20.0202 23.2676 19.963 22.9342L18.8836 16.641L23.4559 12.1841C23.6981 11.948 23.7853 11.5948 23.6807 11.2731C23.5762 10.9514 23.2981 10.717 22.9634 10.6683L16.6446 9.75015L13.8188 4.02442ZM10.7862 10.9557L13.0219 6.42572L15.2576 10.9557C15.387 11.218 15.6373 11.3998 15.9267 11.4418L20.9258 12.1683L17.3084 15.6944C17.099 15.8985 17.0034 16.1927 17.0529 16.4809L17.9068 21.4599L13.4355 19.1091C13.1766 18.973 12.8673 18.973 12.6084 19.1091L8.13703 21.4599L8.99098 16.4809C9.04043 16.1927 8.94485 15.8985 8.7354 15.6944L5.118 12.1683L10.1171 11.4418C10.4066 11.3998 10.6568 11.218 10.7862 10.9557ZM25.2694 5.97276C25.6165 6.31978 25.6166 6.88245 25.2696 7.22954L23.5199 8.97954C23.1729 9.32662 22.6102 9.32668 22.2632 8.97967C21.9161 8.63265 21.916 8.06998 22.263 7.72289L24.0127 5.97289C24.3597 5.62581 24.9224 5.62575 25.2694 5.97276Z" fill=""></path>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const formatNumber = (num: number | undefined | null): string => {
|
||||
if (!num || num === 0) return '0';
|
||||
if (num >= 1000000) return `${(num / 1000000).toFixed(0)}M`;
|
||||
if (num >= 1000) return `${(num / 1000).toFixed(0)}K`;
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
const convertToPricingPlan = (plan: Plan): PricingPlan => {
|
||||
const monthlyPrice = typeof plan.price === 'number' ? plan.price : parseFloat(String(plan.price || 0));
|
||||
const features: string[] = [];
|
||||
|
||||
if (plan.max_sites) features.push(`${plan.max_sites === 999999 ? 'Unlimited' : plan.max_sites} Site${plan.max_sites > 1 ? 's' : ''}`);
|
||||
if (plan.max_users) features.push(`${plan.max_users} Team User${plan.max_users > 1 ? 's' : ''}`);
|
||||
if (plan.included_credits) features.push(`${formatNumber(plan.included_credits)} Monthly Credits`);
|
||||
if (plan.max_content_words) features.push(`${formatNumber(plan.max_content_words)} Words/Month`);
|
||||
if (plan.max_clusters) features.push(`${plan.max_clusters} AI Keyword Clusters`);
|
||||
if (plan.max_content_ideas) features.push(`${formatNumber(plan.max_content_ideas)} Content Ideas`);
|
||||
if (plan.max_images_basic && plan.max_images_premium) {
|
||||
features.push(`${formatNumber(plan.max_images_basic)} Basic / ${formatNumber(plan.max_images_premium)} Premium Images`);
|
||||
}
|
||||
if (plan.max_image_prompts) features.push(`${formatNumber(plan.max_image_prompts)} Image Prompts`);
|
||||
|
||||
// Custom descriptions based on plan name
|
||||
let description = `Perfect for ${plan.name.toLowerCase()} needs`;
|
||||
if (plan.name.toLowerCase().includes('free')) {
|
||||
description = 'Explore core features risk free';
|
||||
} else if (plan.name.toLowerCase().includes('starter')) {
|
||||
description = 'Launch SEO workflows for small teams';
|
||||
} else if (plan.name.toLowerCase().includes('growth')) {
|
||||
description = 'Scale content production with confidence';
|
||||
} else if (plan.name.toLowerCase().includes('scale')) {
|
||||
description = 'Enterprise power for high volume growth';
|
||||
}
|
||||
|
||||
return {
|
||||
id: plan.id,
|
||||
slug: plan.slug,
|
||||
name: plan.name,
|
||||
monthlyPrice: monthlyPrice,
|
||||
price: monthlyPrice,
|
||||
originalPrice: plan.original_price ? (typeof plan.original_price === 'number' ? plan.original_price : parseFloat(String(plan.original_price))) : undefined,
|
||||
period: '/month',
|
||||
description: description,
|
||||
features,
|
||||
buttonText: monthlyPrice === 0 ? 'Free Trial' : 'Choose Plan',
|
||||
highlighted: plan.is_featured || false,
|
||||
annualDiscountPercent: plan.annual_discount_percent || 15,
|
||||
};
|
||||
};
|
||||
|
||||
export default function PricingTablePage() {
|
||||
const [backendPlans, setBackendPlans] = useState<Plan[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchPlans = async () => {
|
||||
try {
|
||||
const data = await getPublicPlans();
|
||||
setBackendPlans(data);
|
||||
setLoading(false);
|
||||
} catch (err) {
|
||||
console.error('Error fetching plans:', err);
|
||||
setError('Failed to load plans');
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
fetchPlans();
|
||||
}, []);
|
||||
|
||||
// Sample plans for variant 1
|
||||
const plans1: PricingPlan[] = [
|
||||
{
|
||||
id: 0,
|
||||
name: 'Free Plan',
|
||||
price: 0.00,
|
||||
period: '/month',
|
||||
description: 'Perfect for free plan needs',
|
||||
features: [
|
||||
'1 Site',
|
||||
'1 Team User',
|
||||
'1K Monthly Credits',
|
||||
'100K Words/Month',
|
||||
'100 AI Keyword Clusters',
|
||||
'300 Content Ideas',
|
||||
'300 Basic / 60 Premium Images',
|
||||
'300 Image Prompts',
|
||||
],
|
||||
buttonText: 'Start Free',
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: 'Starter',
|
||||
price: 5.00,
|
||||
originalPrice: 12.00,
|
||||
period: '/month',
|
||||
description: 'For solo designers & freelancers',
|
||||
features: [
|
||||
'5 website',
|
||||
'500 MB Storage',
|
||||
'Unlimited Sub-Domain',
|
||||
'3 Custom Domain',
|
||||
'Free SSL Certificate',
|
||||
'Unlimited Traffic',
|
||||
],
|
||||
buttonText: 'Choose Starter',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Medium',
|
||||
price: 10.99,
|
||||
originalPrice: 30.00,
|
||||
period: '/month',
|
||||
description: 'For working on commercial projects',
|
||||
features: [
|
||||
'10 website',
|
||||
'1 GB Storage',
|
||||
'Unlimited Sub-Domain',
|
||||
'5 Custom Domain',
|
||||
'Free SSL Certificate',
|
||||
'Unlimited Traffic',
|
||||
],
|
||||
buttonText: 'Choose Starter',
|
||||
highlighted: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Large',
|
||||
price: 15.00,
|
||||
originalPrice: 59.00,
|
||||
period: '/month',
|
||||
description: 'For teams larger than 5 members',
|
||||
features: [
|
||||
'15 website',
|
||||
'10 GB Storage',
|
||||
'Unlimited Sub-Domain',
|
||||
'10 Custom Domain',
|
||||
'Free SSL Certificate',
|
||||
'Unlimited Traffic',
|
||||
],
|
||||
buttonText: 'Choose Starter',
|
||||
},
|
||||
];
|
||||
|
||||
// Sample plans for variant 2
|
||||
const plans2: PricingPlan[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Personal',
|
||||
price: 59.00,
|
||||
period: ' / Lifetime',
|
||||
description: 'For solo designers & freelancers',
|
||||
features: [
|
||||
'5 website',
|
||||
'500 MB Storage',
|
||||
'Unlimited Sub-Domain',
|
||||
'3 Custom Domain',
|
||||
'!Free SSL Certificate',
|
||||
'!Unlimited Traffic',
|
||||
],
|
||||
buttonText: 'Choose Starter',
|
||||
icon: <PersonIcon />,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Professional',
|
||||
price: 199.00,
|
||||
period: ' / Lifetime',
|
||||
description: 'For working on commercial projects',
|
||||
features: [
|
||||
'10 website',
|
||||
'1GB Storage',
|
||||
'Unlimited Sub-Domain',
|
||||
'5 Custom Domain',
|
||||
'Free SSL Certificate',
|
||||
'!Unlimited Traffic',
|
||||
],
|
||||
buttonText: 'Choose This Plan',
|
||||
icon: <BriefcaseIcon />,
|
||||
highlighted: true,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Enterprise',
|
||||
price: 599.00,
|
||||
period: ' / Lifetime',
|
||||
description: 'For teams larger than 5 members',
|
||||
features: [
|
||||
'15 website',
|
||||
'10GB Storage',
|
||||
'Unlimited Sub-Domain',
|
||||
'10 Custom Domain',
|
||||
'Free SSL Certificate',
|
||||
'Unlimited Traffic',
|
||||
],
|
||||
buttonText: 'Choose This Plan',
|
||||
icon: <StarIcon />,
|
||||
},
|
||||
];
|
||||
|
||||
// Sample plans for variant 3
|
||||
const plans3: PricingPlan[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Personal',
|
||||
price: 'Free',
|
||||
period: 'For a Lifetime',
|
||||
description: 'Perfect plan for Starters',
|
||||
features: [
|
||||
'Unlimited Projects',
|
||||
'Share with 5 team members',
|
||||
'Sync across devices',
|
||||
],
|
||||
buttonText: 'Current Plan',
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Professional',
|
||||
price: 99.00,
|
||||
period: '/year',
|
||||
description: 'For users who want to do more',
|
||||
features: [
|
||||
'Unlimited Projects',
|
||||
'Share with 5 team members',
|
||||
'Sync across devices',
|
||||
'30 days version history',
|
||||
],
|
||||
buttonText: 'Try for Free',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Team',
|
||||
price: 299,
|
||||
period: ' /year',
|
||||
description: 'Your entire team in one place',
|
||||
features: [
|
||||
'Unlimited Projects',
|
||||
'Share with 5 team members',
|
||||
'Sync across devices',
|
||||
'Sharing permissions',
|
||||
'Admin tools',
|
||||
],
|
||||
buttonText: 'Try for Free',
|
||||
recommended: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Enterprise',
|
||||
price: 'Custom',
|
||||
period: 'Reach out for a quote',
|
||||
description: 'Run your company on your terms',
|
||||
features: [
|
||||
'Unlimited Projects',
|
||||
'Share with 5 team members',
|
||||
'Sync across devices',
|
||||
'Sharing permissions',
|
||||
'User provisioning (SCIM)',
|
||||
'Advanced security',
|
||||
],
|
||||
buttonText: 'Try for Free',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Pricing Tables | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Pricing Tables page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Pricing Table 1 - Dynamic (Backend Plans)">
|
||||
{loading && (
|
||||
<div className="text-center py-12">
|
||||
<div className="inline-block animate-spin rounded-full h-12 w-12 border-b-2 border-brand-500"></div>
|
||||
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading backend plans...</p>
|
||||
</div>
|
||||
)}
|
||||
{error && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-red-600">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{!loading && !error && backendPlans.length > 0 && (
|
||||
<PricingTable1
|
||||
plans={backendPlans.map(convertToPricingPlan)}
|
||||
showToggle={true}
|
||||
onPlanSelect={(plan) => console.log('Selected backend plan:', plan)}
|
||||
/>
|
||||
)}
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Pricing Table 1">
|
||||
<PricingTable1
|
||||
plans={plans1}
|
||||
showToggle={true}
|
||||
onPlanSelect={(plan) => console.log('Selected plan:', plan)}
|
||||
/>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Pricing Table 2">
|
||||
<PricingTable
|
||||
variant="2"
|
||||
plans={plans2}
|
||||
onPlanSelect={(plan) => console.log('Selected plan:', plan)}
|
||||
/>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Pricing Table 3">
|
||||
<PricingTable
|
||||
variant="3"
|
||||
plans={plans3}
|
||||
onPlanSelect={(plan) => console.log('Selected plan:', plan)}
|
||||
/>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import { ProgressBar } from "../../../components/ui/progress";
|
||||
|
||||
export default function Progressbar() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Progressbar Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Progressbar Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Progress Bar Sizes">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
Small
|
||||
</p>
|
||||
<ProgressBar value={75} size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
Medium
|
||||
</p>
|
||||
<ProgressBar value={75} size="md" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
Large
|
||||
</p>
|
||||
<ProgressBar value={75} size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Progress Bar Colors">
|
||||
<div className="space-y-6">
|
||||
<ProgressBar value={60} color="primary" showLabel />
|
||||
<ProgressBar value={75} color="success" showLabel />
|
||||
<ProgressBar value={45} color="error" showLabel />
|
||||
<ProgressBar value={80} color="warning" showLabel />
|
||||
<ProgressBar value={65} color="info" showLabel />
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Progress Bar with Label">
|
||||
<div className="space-y-6">
|
||||
<ProgressBar
|
||||
value={50}
|
||||
color="primary"
|
||||
showLabel
|
||||
label="Upload Progress"
|
||||
/>
|
||||
<ProgressBar
|
||||
value={75}
|
||||
color="success"
|
||||
showLabel
|
||||
label="Download Progress"
|
||||
/>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import { Ribbon } from "../../../components/ui/ribbon";
|
||||
|
||||
export default function Ribbons() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Ribbons Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Ribbons Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-5 sm:gap-6 lg:grid-cols-2">
|
||||
<ComponentCard title="Rounded Ribbon">
|
||||
<Ribbon text="Popular" variant="rounded" color="primary">
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="p-5 pt-16">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Lorem ipsum dolor sit amet consectetur. Eget nulla suscipit
|
||||
arcu rutrum amet vel nec fringilla vulputate. Sed aliquam
|
||||
fringilla vulputate imperdiet arcu natoque purus ac nec
|
||||
ultricies nulla ultrices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Ribbon>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Filled Ribbon">
|
||||
<Ribbon text="New" variant="filled" color="primary">
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="p-5 pt-16">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Lorem ipsum dolor sit amet consectetur. Eget nulla suscipit
|
||||
arcu rutrum amet vel nec fringilla vulputate. Sed aliquam
|
||||
fringilla vulputate imperdiet arcu natoque purus ac nec
|
||||
ultricies nulla ultrices.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Ribbon>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Ribbon with Different Colors">
|
||||
<div className="space-y-4">
|
||||
<Ribbon text="Success" variant="rounded" color="success">
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="p-5 pt-16">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Success ribbon example.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Ribbon>
|
||||
<Ribbon text="Warning" variant="rounded" color="warning">
|
||||
<div className="rounded-xl border border-gray-200 dark:border-gray-800 dark:bg-white/[0.03]">
|
||||
<div className="p-5 pt-16">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||
Warning ribbon example.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Ribbon>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import { Spinner } from "../../../components/ui/spinner";
|
||||
|
||||
export default function Spinners() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Spinners Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Spinners Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Size Variants">
|
||||
<div className="flex flex-wrap items-center gap-6">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
Small
|
||||
</p>
|
||||
<Spinner size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
Medium
|
||||
</p>
|
||||
<Spinner size="md" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
Large
|
||||
</p>
|
||||
<Spinner size="lg" />
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
|
||||
<ComponentCard title="Color Variants">
|
||||
<div className="flex flex-wrap items-center gap-6">
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
Primary
|
||||
</p>
|
||||
<Spinner color="primary" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
Success
|
||||
</p>
|
||||
<Spinner color="success" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
Error
|
||||
</p>
|
||||
<Spinner color="error" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
Warning
|
||||
</p>
|
||||
<Spinner color="warning" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mb-2">
|
||||
Info
|
||||
</p>
|
||||
<Spinner color="info" />
|
||||
</div>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import { Tabs, TabList, Tab, TabPanel } from "../../../components/ui/tabs";
|
||||
|
||||
export default function TabsPage() {
|
||||
const [activeTab, setActiveTab] = useState("tab1");
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Tabs Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Tabs Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Default Tabs">
|
||||
<Tabs defaultTab="tab1" onChange={setActiveTab}>
|
||||
<TabList>
|
||||
<Tab
|
||||
tabId="tab1"
|
||||
isActive={activeTab === "tab1"}
|
||||
onClick={() => setActiveTab("tab1")}
|
||||
>
|
||||
Tab 1
|
||||
</Tab>
|
||||
<Tab
|
||||
tabId="tab2"
|
||||
isActive={activeTab === "tab2"}
|
||||
onClick={() => setActiveTab("tab2")}
|
||||
>
|
||||
Tab 2
|
||||
</Tab>
|
||||
<Tab
|
||||
tabId="tab3"
|
||||
isActive={activeTab === "tab3"}
|
||||
onClick={() => setActiveTab("tab3")}
|
||||
>
|
||||
Tab 3
|
||||
</Tab>
|
||||
</TabList>
|
||||
<div className="mt-4">
|
||||
<TabPanel tabId="tab1" isActive={activeTab === "tab1"}>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Content for Tab 1
|
||||
</p>
|
||||
</TabPanel>
|
||||
<TabPanel tabId="tab2" isActive={activeTab === "tab2"}>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Content for Tab 2
|
||||
</p>
|
||||
</TabPanel>
|
||||
<TabPanel tabId="tab3" isActive={activeTab === "tab3"}>
|
||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
||||
Content for Tab 3
|
||||
</p>
|
||||
</TabPanel>
|
||||
</div>
|
||||
</Tabs>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import { Tooltip } from "../../../components/ui/tooltip";
|
||||
import Button from "../../../components/ui/button/Button";
|
||||
|
||||
export default function Tooltips() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Tooltips Dashboard | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Tooltips Dashboard page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Tooltip Placements">
|
||||
<div className="flex flex-wrap items-center gap-6">
|
||||
<Tooltip text="Tooltip Top" placement="top">
|
||||
<Button>Tooltip Top</Button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Tooltip Right" placement="right">
|
||||
<Button>Tooltip Right</Button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Tooltip Bottom" placement="bottom">
|
||||
<Button>Tooltip Bottom</Button>
|
||||
</Tooltip>
|
||||
<Tooltip text="Tooltip Left" placement="left">
|
||||
<Button>Tooltip Left</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import ComponentCard from "../../../components/common/ComponentCard";
|
||||
import PageMeta from "../../../components/common/PageMeta";
|
||||
import FourIsToThree from "../../../components/ui/videos/FourIsToThree";
|
||||
import OneIsToOne from "../../../components/ui/videos/OneIsToOne";
|
||||
import SixteenIsToNine from "../../../components/ui/videos/SixteenIsToNine";
|
||||
import TwentyOneIsToNine from "../../../components/ui/videos/TwentyOneIsToNine";
|
||||
|
||||
export default function Videos() {
|
||||
return (
|
||||
<>
|
||||
<PageMeta
|
||||
title="React.js Videos Tabs | TailAdmin - React.js Admin Dashboard Template"
|
||||
description="This is React.js Videos page for TailAdmin - React.js Tailwind CSS Admin Dashboard Template"
|
||||
/>
|
||||
<div className="grid grid-cols-1 gap-5 sm:gap-6 xl:grid-cols-2">
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Video Ratio 16:9">
|
||||
<SixteenIsToNine />
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Video Ratio 4:3">
|
||||
<FourIsToThree />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
<div className="space-y-5 sm:space-y-6">
|
||||
<ComponentCard title="Video Ratio 21:9">
|
||||
<TwentyOneIsToNine />
|
||||
</ComponentCard>
|
||||
<ComponentCard title="Video Ratio 1:1">
|
||||
<OneIsToOne />
|
||||
</ComponentCard>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user