Initial commit: igny8 project

This commit is contained in:
igny8
2025-11-09 10:27:02 +00:00
commit 60b8188111
27265 changed files with 4360521 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { Card } from '../../components/ui/card';
export default function AISettings() {
const toast = useToast();
const [settings, setSettings] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
try {
setLoading(true);
const response = await fetchAPI('/v1/system/settings/ai/');
setSettings(response.results || []);
} catch (error: any) {
toast.error(`Failed to load AI settings: ${error.message}`);
} finally {
setLoading(false);
}
};
return (
<div className="p-6">
<PageMeta title="AI Settings" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">AI Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">AI-specific configuration</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<p className="text-gray-600 dark:text-gray-400">AI settings management interface coming soon.</p>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { Card } from '../../components/ui/card';
export default function AccountSettings() {
const toast = useToast();
const [settings, setSettings] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
try {
setLoading(true);
const response = await fetchAPI('/v1/system/settings/account/');
setSettings(response.results || []);
} catch (error: any) {
toast.error(`Failed to load account settings: ${error.message}`);
} finally {
setLoading(false);
}
};
return (
<div className="p-6">
<PageMeta title="Account Settings" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Account Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Account-level configuration</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<p className="text-gray-600 dark:text-gray-400">Account settings management interface coming soon.</p>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,112 @@
import { useState, useEffect } from 'react';
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import { useSettingsStore } from '../../store/settingsStore';
import { useToast } from '../../components/ui/toast/ToastContainer';
import Button from '../../components/ui/button/Button';
import Label from '../../components/form/Label';
export default function GeneralSettings() {
const toast = useToast();
const { accountSettings, loading, loadAccountSettings, updateAccountSetting } = useSettingsStore();
// Form state
const [tableSettings, setTableSettings] = useState({
records_per_page: 20,
default_sort: 'created_at',
default_sort_direction: 'desc',
});
useEffect(() => {
loadAccountSettings();
}, [loadAccountSettings]);
useEffect(() => {
if (accountSettings['table_settings']) {
setTableSettings(accountSettings['table_settings'].config);
}
}, [accountSettings]);
const handleSave = async () => {
try {
await updateAccountSetting('table_settings', tableSettings);
toast.success('Settings saved successfully');
} catch (error: any) {
toast.error(`Failed to save settings: ${error.message}`);
}
};
return (
<>
<PageMeta title="General Settings - IGNY8" description="Plugin configuration" />
<ComponentCard title="General Settings" desc="Configure plugin settings, automation, and table preferences">
<div className="space-y-6">
{/* Table Settings */}
<div className="space-y-4">
<h3 className="text-lg font-semibold">Table Settings</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="records_per_page">Records Per Page</Label>
<input
id="records_per_page"
type="number"
min="5"
max="100"
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={tableSettings.records_per_page}
onChange={(e) => setTableSettings({
...tableSettings,
records_per_page: parseInt(e.target.value) || 20
})}
/>
</div>
<div>
<Label htmlFor="default_sort">Default Sort Field</Label>
<input
id="default_sort"
type="text"
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
value={tableSettings.default_sort}
onChange={(e) => setTableSettings({
...tableSettings,
default_sort: e.target.value
})}
/>
</div>
<div>
<Label htmlFor="default_sort_direction">Default Sort Direction</Label>
<select
id="default_sort_direction"
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:focus:border-brand-800"
value={tableSettings.default_sort_direction}
onChange={(e) => setTableSettings({
...tableSettings,
default_sort_direction: e.target.value as 'asc' | 'desc'
})}
>
<option value="asc">Ascending</option>
<option value="desc">Descending</option>
</select>
</div>
</div>
</div>
<div className="flex justify-end">
<Button
onClick={handleSave}
disabled={loading}
className="px-6"
>
{loading ? 'Saving...' : 'Save Settings'}
</Button>
</div>
</div>
</ComponentCard>
</>
);
}

View File

@@ -0,0 +1,22 @@
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
export default function ImportExport() {
return (
<>
<PageMeta title="Import/Export - IGNY8" description="Data management" />
<ComponentCard title="Coming Soon" desc="Data management">
<div className="text-center py-8">
<p className="text-gray-600 dark:text-gray-400">
Import/Export - Coming Soon
</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
Import and export data, manage backups, and transfer content
</p>
</div>
</ComponentCard>
</>
);
}

View File

@@ -0,0 +1,64 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchIndustries, Industry } from '../../services/api';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
export default function Industries() {
const toast = useToast();
const [industries, setIndustries] = useState<Industry[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadIndustries();
}, []);
const loadIndustries = async () => {
try {
setLoading(true);
const response = await fetchIndustries();
setIndustries(response.industries || []);
} catch (error: any) {
toast.error(`Failed to load industries: ${error.message}`);
} finally {
setLoading(false);
}
};
return (
<div className="p-6">
<PageMeta title="Industries" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Industries</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Manage global industry templates (Admin Only)</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{industries.map((industry) => (
<Card key={industry.id} className="p-6">
<div className="flex justify-between items-start mb-4">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white">{industry.name}</h3>
<Badge variant="light" color={industry.is_active ? 'success' : 'dark'}>
{industry.is_active ? 'Active' : 'Inactive'}
</Badge>
</div>
{industry.description && (
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2">{industry.description}</p>
)}
<p className="text-sm text-gray-500 dark:text-gray-400">
Sectors: {industry.sectors_count || 0}
</p>
</Card>
))}
</div>
)}
</div>
);
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,48 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { Card } from '../../components/ui/card';
export default function ModuleSettings() {
const toast = useToast();
const [settings, setSettings] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
try {
setLoading(true);
const response = await fetchAPI('/v1/system/settings/modules/');
setSettings(response.results || []);
} catch (error: any) {
toast.error(`Failed to load module settings: ${error.message}`);
} finally {
setLoading(false);
}
};
return (
<div className="p-6">
<PageMeta title="Module Settings" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Module Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Module-specific configuration</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<p className="text-gray-600 dark:text-gray-400">Module settings management interface coming soon.</p>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,213 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { PricingTable, PricingPlan } from '../../components/ui/pricing-table';
interface Plan {
id: number;
name: string;
slug: string;
price: string | number;
billing_cycle: string;
is_active: boolean;
max_users: number;
max_sites: number;
max_keywords: number;
max_clusters: number;
max_content_ideas: number;
monthly_word_count_limit: number;
monthly_ai_credit_limit: number;
monthly_image_count: number;
daily_content_tasks: number;
daily_ai_request_limit: number;
daily_image_generation_limit: number;
included_credits: number;
image_model_choices: string[];
features: string[];
}
interface PlanResponse {
count: number;
next: string | null;
previous: string | null;
results: Plan[];
}
// Helper function to format numbers with commas
const formatNumber = (num: number): string => {
return num.toLocaleString();
};
// Helper function to format word count
const formatWordCount = (num: number): string => {
if (num >= 1000000) {
return `${(num / 1000000).toFixed(1)}M`;
}
if (num >= 1000) {
return `${(num / 1000).toFixed(0)}K`;
}
return num.toString();
};
// Extract major features from plan data
const extractFeatures = (plan: Plan): string[] => {
const features: string[] = [];
// Sites and Users
features.push(`${plan.max_sites} ${plan.max_sites === 1 ? 'Site' : 'Sites'}`);
features.push(`${plan.max_users} ${plan.max_users === 1 ? 'User' : 'Users'}`);
// Planner features
features.push(`${formatNumber(plan.max_keywords)} Keywords`);
features.push(`${formatNumber(plan.max_clusters)} Clusters`);
features.push(`${formatNumber(plan.max_content_ideas)} Content Ideas`);
// Writer features
features.push(`${formatWordCount(plan.monthly_word_count_limit)} Words/Month`);
features.push(`${plan.daily_content_tasks} Daily Content Tasks`);
// Image features
features.push(`${plan.monthly_image_count} Images/Month`);
if (plan.image_model_choices && plan.image_model_choices.length > 0) {
const models = plan.image_model_choices.map((m: string) => m.toUpperCase()).join(', ');
features.push(`${models} Image Models`);
}
// AI Credits
features.push(`${formatNumber(plan.included_credits)} AI Credits Included`);
features.push(`${formatNumber(plan.monthly_ai_credit_limit)} Monthly AI Credit Limit`);
// Feature flags
if (plan.features && Array.isArray(plan.features)) {
if (plan.features.includes('ai_writer')) {
features.push('AI Writer');
}
if (plan.features.includes('image_gen')) {
features.push('Image Generation');
}
if (plan.features.includes('auto_publish')) {
features.push('Auto Publish');
}
if (plan.features.includes('custom_prompts')) {
features.push('Custom Prompts');
}
}
return features;
};
// Transform Plan to PricingPlan
const transformPlanToPricingPlan = (plan: Plan, index: number, totalPlans: number): PricingPlan => {
const monthlyPrice = typeof plan.price === 'number' ? plan.price : parseFloat(String(plan.price || 0));
// Only highlight Growth plan (by slug)
const highlighted = plan.slug.toLowerCase() === 'growth';
return {
id: plan.id,
name: plan.name,
monthlyPrice: monthlyPrice,
price: monthlyPrice, // Will be calculated by component based on period
period: '/month',
description: getPlanDescription(plan),
features: extractFeatures(plan),
buttonText: 'Choose Plan',
highlighted: highlighted,
};
};
// Get plan description based on plan name or features
const getPlanDescription = (plan: Plan): string => {
const slug = plan.slug.toLowerCase();
if (slug.includes('free')) {
return 'Perfect for getting started';
} else if (slug.includes('starter')) {
return 'For solo designers & freelancers';
} else if (slug.includes('growth')) {
return 'For growing businesses';
} else if (slug.includes('scale') || slug.includes('enterprise')) {
return 'For teams and large organizations';
}
return 'Choose the perfect plan for your needs';
};
export default function Plans() {
const toast = useToast();
const [plans, setPlans] = useState<Plan[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadPlans();
}, []);
const loadPlans = async () => {
try {
setLoading(true);
const response: PlanResponse = await fetchAPI('/v1/auth/plans/');
// Filter only active plans and sort by price
const activePlans = (response.results || [])
.filter((plan) => plan.is_active)
.sort((a, b) => {
const priceA = typeof a.price === 'number' ? a.price : parseFloat(String(a.price || 0));
const priceB = typeof b.price === 'number' ? b.price : parseFloat(String(b.price || 0));
return priceA - priceB;
});
setPlans(activePlans);
} catch (error: any) {
toast.error(`Failed to load plans: ${error.message}`);
} finally {
setLoading(false);
}
};
const handlePlanSelect = (plan: PricingPlan) => {
console.log('Selected plan:', plan);
// TODO: Implement plan selection/subscription logic
toast.success(`Selected plan: ${plan.name}`);
};
const pricingPlans: PricingPlan[] = plans.map((plan, index) =>
transformPlanToPricingPlan(plan, index, plans.length)
);
return (
<div className="p-6">
<PageMeta title="Plans" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Plans</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">
Choose the perfect plan for your needs. All plans include our core features.
</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading plans...</div>
</div>
) : pricingPlans.length === 0 ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">No active plans available</div>
</div>
) : (
<>
<PricingTable
variant="1"
title="Flexible Plans Tailored to Fit Your Unique Needs!"
plans={pricingPlans}
showToggle={true}
onPlanSelect={handlePlanSelect}
/>
{/* Future: Add "View All Features" section here */}
<div className="mt-8 text-center">
<p className="text-sm text-gray-500 dark:text-gray-400">
Need more details? View all features and limits for each plan.
</p>
{/* TODO: Add expandable feature list component */}
</div>
</>
)}
</div>
);
}

View File

@@ -0,0 +1,597 @@
import { useState, useEffect, useCallback } from 'react';
import PageMeta from '../../components/common/PageMeta';
import SiteCard from '../../components/common/SiteCard';
import FormModal, { FormField } from '../../components/common/FormModal';
import Button from '../../components/ui/button/Button';
import { useToast } from '../../components/ui/toast/ToastContainer';
import Alert from '../../components/ui/alert/Alert';
import {
fetchSites,
createSite,
updateSite,
deleteSite,
setActiveSite,
selectSectorsForSite,
fetchIndustries,
fetchSiteSectors,
Site,
Industry,
Sector,
} from '../../services/api';
import Badge from '../../components/ui/badge/Badge';
// Site Icon SVG
const SiteIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40" fill="none">
<rect width="40" height="40" rx="8" fill="#3B82F6"/>
<path d="M12 16L20 10L28 16V28C28 28.5304 27.7893 29.0391 27.4142 29.4142C27.0391 29.7893 26.5304 30 26 30H14C13.4696 30 12.9609 29.7893 12.5858 29.4142C12.2107 29.0391 12 28.5304 12 28V16Z" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M16 30V20H24V30" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
export default function Sites() {
const toast = useToast();
const [sites, setSites] = useState<Site[]>([]);
const [loading, setLoading] = useState(true);
const [selectedSite, setSelectedSite] = useState<Site | null>(null);
const [showSiteModal, setShowSiteModal] = useState(false);
const [showSectorsModal, setShowSectorsModal] = useState(false);
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [togglingSiteId, setTogglingSiteId] = useState<number | null>(null);
const [industries, setIndustries] = useState<Industry[]>([]);
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
const [selectedSectors, setSelectedSectors] = useState<string[]>([]);
const [isSelectingSectors, setIsSelectingSectors] = useState(false);
// Form state for site creation/editing
const [formData, setFormData] = useState({
name: '',
domain: '',
description: '',
is_active: true, // Default to true to match backend model default
});
// Load sites and industries
useEffect(() => {
loadSites();
loadIndustries();
}, []);
const loadSites = async () => {
try {
setLoading(true);
const response = await fetchSites();
setSites(response.results || []);
} catch (error: any) {
toast.error(`Failed to load sites: ${error.message}`);
} finally {
setLoading(false);
}
};
const loadIndustries = async () => {
try {
const response = await fetchIndustries();
setIndustries(response.industries || []);
} catch (error: any) {
toast.error(`Failed to load industries: ${error.message}`);
}
};
const handleToggle = async (siteId: number, enabled: boolean) => {
// Prevent multiple simultaneous toggle operations
if (togglingSiteId !== null) {
toast.error('Please wait for the current operation to complete');
return;
}
try {
setTogglingSiteId(siteId);
if (enabled) {
// Activate site (multiple sites can be active simultaneously)
await setActiveSite(siteId);
toast.success('Site activated successfully');
} else {
// Deactivate site - only this specific site
const site = sites.find(s => s.id === siteId);
if (site) {
await updateSite(siteId, { is_active: false });
toast.success('Site deactivated successfully');
}
}
await loadSites();
} catch (error: any) {
toast.error(`Failed to update site: ${error.message}`);
} finally {
setTogglingSiteId(null);
}
};
const handleSettings = (site: Site) => {
setSelectedSite(site);
setShowSectorsModal(true);
// Load current sectors for this site
loadSiteSectors(site);
};
const loadSiteSectors = async (site: Site) => {
try {
const sectors = await fetchSiteSectors(site.id);
const sectorSlugs = sectors.map((s: any) => s.slug);
setSelectedSectors(sectorSlugs);
// Use site's industry if available, otherwise try to determine from sectors
if (site.industry_slug) {
setSelectedIndustry(site.industry_slug);
} else {
// Fallback: try to determine industry from sectors
for (const industry of industries) {
const matchingSectors = industry.sectors.filter(s => sectorSlugs.includes(s.slug));
if (matchingSectors.length > 0) {
setSelectedIndustry(industry.slug);
break;
}
}
}
} catch (error: any) {
console.error('Failed to load site sectors:', error);
}
};
const handleDetails = (site: Site) => {
setSelectedSite(site);
setFormData({
name: site.name || '',
domain: site.domain || '',
description: site.description || '',
is_active: site.is_active || false,
});
setShowDetailsModal(true);
};
const handleSaveDetails = async () => {
if (!selectedSite) return;
try {
setIsSaving(true);
// Normalize domain before sending
const normalizedFormData = {
...formData,
domain: formData.domain ? normalizeDomain(formData.domain) : formData.domain,
};
await updateSite(selectedSite.id, normalizedFormData);
toast.success('Site updated successfully');
setShowDetailsModal(false);
await loadSites();
} catch (error: any) {
toast.error(`Failed to update site: ${error.message}`);
} finally {
setIsSaving(false);
}
};
const handleCreateSite = () => {
setSelectedSite(null);
setFormData({
name: '',
domain: '',
description: '',
is_active: true, // Default to true to match backend model default
});
setShowSiteModal(true);
};
const handleEditSite = (site: Site) => {
setSelectedSite(site);
setFormData({
name: site.name || '',
domain: site.domain || '',
description: site.description || '',
is_active: site.is_active || false,
});
setShowSiteModal(true);
};
// Helper function to normalize domain URL
const normalizeDomain = (domain: string): string => {
if (!domain || !domain.trim()) {
return domain;
}
const trimmed = domain.trim();
// If it already starts with https://, keep it as is
if (trimmed.startsWith('https://')) {
return trimmed;
}
// If it starts with http://, replace with https://
if (trimmed.startsWith('http://')) {
return trimmed.replace('http://', 'https://');
}
// Otherwise, add https://
return `https://${trimmed}`;
};
const handleSaveSite = async () => {
try {
setIsSaving(true);
// Normalize domain before sending
const normalizedFormData = {
...formData,
domain: formData.domain ? normalizeDomain(formData.domain) : formData.domain,
};
if (selectedSite) {
// Update existing site
await updateSite(selectedSite.id, normalizedFormData);
toast.success('Site updated successfully');
} else {
// Create new site
const newSite = await createSite({
...normalizedFormData,
is_active: normalizedFormData.is_active || false,
});
toast.success('Site created successfully');
// If this is the first site or user wants it active, activate it
if (sites.length === 0 || normalizedFormData.is_active) {
await setActiveSite(newSite.id);
}
}
setShowSiteModal(false);
setSelectedSite(null);
setFormData({
name: '',
domain: '',
description: '',
is_active: false,
});
await loadSites();
} catch (error: any) {
toast.error(`Failed to save site: ${error.message}`);
} finally {
setIsSaving(false);
}
};
const handleSelectSectors = async () => {
if (!selectedSite || !selectedIndustry || selectedSectors.length === 0) {
toast.error('Please select an industry and at least one sector');
return;
}
if (selectedSectors.length > 5) {
toast.error('Maximum 5 sectors allowed per site');
return;
}
try {
setIsSelectingSectors(true);
const result = await selectSectorsForSite(
selectedSite.id,
selectedIndustry,
selectedSectors
);
toast.success(result.message || 'Sectors selected successfully');
setShowSectorsModal(false);
await loadSites();
} catch (error: any) {
toast.error(`Failed to select sectors: ${error.message}`);
} finally {
setIsSelectingSectors(false);
}
};
const handleDeleteSite = async (site: Site) => {
if (!window.confirm(`Are you sure you want to delete "${site.name}"? This action cannot be undone.`)) {
return;
}
try {
await deleteSite(site.id);
toast.success('Site deleted successfully');
await loadSites();
if (showDetailsModal) {
setShowDetailsModal(false);
}
} catch (error: any) {
toast.error(`Failed to delete site: ${error.message}`);
}
};
const getSiteFormFields = (): FormField[] => [
{
key: 'name',
label: 'Site Name',
type: 'text',
value: formData.name,
onChange: (value: any) => setFormData({ ...formData, name: value }),
required: true,
placeholder: 'Enter site name',
},
{
key: 'domain',
label: 'Domain',
type: 'text',
value: formData.domain,
onChange: (value: any) => setFormData({ ...formData, domain: value }),
required: false,
placeholder: 'example.com (https:// will be added automatically)',
},
{
key: 'description',
label: 'Description',
type: 'textarea',
value: formData.description,
onChange: (value: any) => setFormData({ ...formData, description: value }),
required: false,
placeholder: 'Enter site description',
rows: 4,
},
{
key: 'is_active',
label: 'Set as Active Site',
type: 'select',
value: formData.is_active ? 'true' : 'false',
onChange: (value: any) => setFormData({ ...formData, is_active: value === 'true' }),
required: false,
options: [
{ value: 'true', label: 'Active' },
{ value: 'false', label: 'Inactive' },
],
},
];
const getIndustrySectors = () => {
if (!selectedIndustry) return [];
const industry = industries.find(i => i.slug === selectedIndustry);
return industry?.sectors || [];
};
if (loading) {
return (
<div className="flex h-screen items-center justify-center">
<div className="text-center">
<div className="mb-4 h-8 w-8 animate-spin rounded-full border-4 border-gray-300 border-t-blue-600 mx-auto"></div>
<p className="text-gray-600 dark:text-gray-400">Loading sites...</p>
</div>
</div>
);
}
return (
<>
<PageMeta title="Sites Management" description="Manage your sites and configure industries and sectors" />
<div className="space-y-8">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Sites Management</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Manage your sites, configure industries, and select sectors. Multiple sites can be active simultaneously.
</p>
</div>
<Button onClick={handleCreateSite} variant="primary">
+ Add Site
</Button>
</div>
{/* Info Alert */}
<Alert
variant="info"
title="Sites Configuration"
message="Each site can have up to 5 sectors selected from 15 major industries. Keywords and clusters are automatically associated with sectors. Multiple sites can be active simultaneously."
/>
{/* Sites Grid */}
{sites.length === 0 ? (
<div className="rounded-2xl border border-gray-200 bg-white p-12 text-center dark:border-gray-800 dark:bg-white/3">
<SiteIcon />
<h3 className="mt-4 text-lg font-semibold text-gray-900 dark:text-white">
No sites yet
</h3>
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
Create your first site to get started
</p>
<Button onClick={handleCreateSite} variant="primary" className="mt-4">
Create Site
</Button>
</div>
) : (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 xl:grid-cols-3">
{sites.map((site) => (
<SiteCard
key={site.id}
site={site}
icon={<SiteIcon />}
onToggle={handleToggle}
onSettings={handleSettings}
onDetails={handleDetails}
isToggling={togglingSiteId === site.id}
/>
))}
</div>
)}
{/* Create/Edit Site Modal */}
<FormModal
isOpen={showSiteModal}
onClose={() => {
setShowSiteModal(false);
setSelectedSite(null);
setFormData({
name: '',
domain: '',
description: '',
is_active: false,
});
}}
onSubmit={handleSaveSite}
title={selectedSite ? 'Edit Site' : 'Create New Site'}
submitLabel={selectedSite ? 'Update Site' : 'Create Site'}
fields={getSiteFormFields()}
isLoading={isSaving}
/>
{/* Sectors Selection Modal */}
<FormModal
isOpen={showSectorsModal}
onClose={() => setShowSectorsModal(false)}
onSubmit={handleSelectSectors}
title={selectedSite ? `Configure Sectors for ${selectedSite.name}` : 'Configure Sectors'}
submitLabel={isSelectingSectors ? 'Saving...' : 'Save Sectors'}
cancelLabel="Cancel"
isLoading={isSelectingSectors}
className="max-w-2xl"
customBody={
<div className="space-y-6">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Select Industry
</label>
<select
value={selectedIndustry}
onChange={(e) => {
setSelectedIndustry(e.target.value);
setSelectedSectors([]); // Reset sectors when industry changes
}}
className="h-9 w-full rounded-lg border border-gray-300 bg-transparent px-3 py-2 text-sm shadow-theme-xs text-gray-800 placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-700 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800"
>
<option value="">Select an industry...</option>
{industries.map((industry) => (
<option key={industry.slug} value={industry.slug}>
{industry.name}
</option>
))}
</select>
{selectedIndustry && (
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
{industries.find(i => i.slug === selectedIndustry)?.description}
</p>
)}
</div>
{selectedIndustry && (
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Select Sectors (max 5)
</label>
<div className="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-lg p-4 dark:border-gray-700">
{getIndustrySectors().map((sector) => (
<label
key={sector.slug}
className="flex items-start space-x-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 cursor-pointer"
>
<input
type="checkbox"
checked={selectedSectors.includes(sector.slug)}
onChange={(e) => {
if (e.target.checked) {
if (selectedSectors.length >= 5) {
toast.error('Maximum 5 sectors allowed per site');
return;
}
setSelectedSectors([...selectedSectors, sector.slug]);
} else {
setSelectedSectors(selectedSectors.filter(s => s !== sector.slug));
}
}}
className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<div className="flex-1">
<div className="font-medium text-sm text-gray-900 dark:text-white">
{sector.name}
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-1">
{sector.description}
</div>
</div>
</label>
))}
</div>
<p className="mt-2 text-xs text-gray-500 dark:text-gray-400">
Selected: {selectedSectors.length} / 5 sectors
</p>
</div>
)}
</div>
}
customFooter={
<div className="flex justify-end gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
<Button
type="button"
variant="outline"
onClick={() => setShowSectorsModal(false)}
disabled={isSelectingSectors}
>
Cancel
</Button>
<Button
type="submit"
variant="primary"
disabled={!selectedIndustry || selectedSectors.length === 0 || isSelectingSectors}
>
{isSelectingSectors ? 'Saving...' : 'Save Sectors'}
</Button>
</div>
}
/>
{/* Site Details Modal - Editable */}
{selectedSite && (
<FormModal
isOpen={showDetailsModal}
onClose={() => {
setShowDetailsModal(false);
setSelectedSite(null);
}}
onSubmit={handleSaveDetails}
title={`Edit Site: ${selectedSite.name}`}
submitLabel="Save Changes"
fields={getSiteFormFields()}
isLoading={isSaving}
customFooter={
<div className="flex justify-between items-center pt-4 border-t border-gray-200 dark:border-gray-700">
<Button
variant="danger"
onClick={() => {
if (selectedSite) {
handleDeleteSite(selectedSite);
}
}}
disabled={isSaving}
>
Delete Site
</Button>
<div className="flex gap-3">
<Button
variant="outline"
onClick={() => {
setShowDetailsModal(false);
setSelectedSite(null);
}}
disabled={isSaving}
>
Cancel
</Button>
<Button
variant="primary"
onClick={handleSaveDetails}
disabled={isSaving}
>
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
}
/>
)}
</div>
</>
);
}

View File

@@ -0,0 +1,323 @@
import { useState, useEffect } from "react";
import PageMeta from "../../components/common/PageMeta";
import ComponentCard from "../../components/common/ComponentCard";
import { fetchAPI } from "../../services/api";
interface SystemStatus {
timestamp: string;
system: {
cpu: { usage_percent: number; cores: number; status: string };
memory: { total_gb: number; used_gb: number; available_gb: number; usage_percent: number; status: string };
disk: { total_gb: number; used_gb: number; free_gb: number; usage_percent: number; status: string };
};
database: {
connected: boolean;
version: string;
size: string;
active_connections: number;
status: string;
};
redis: {
connected: boolean;
status: string;
};
celery: {
workers: string[];
worker_count: number;
tasks: { active: number; scheduled: number; reserved: number };
status: string;
};
processes: {
by_stack: {
[key: string]: { count: number; cpu: number; memory_mb: number };
};
};
modules: {
planner: { keywords: number; clusters: number; content_ideas: number };
writer: { tasks: number; images: number };
};
}
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 'critical': return 'text-red-600 dark:text-red-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 'critical': return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
default: return 'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
}
};
export default function Status() {
const [status, setStatus] = useState<SystemStatus | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchStatus = async () => {
try {
const data = await fetchAPI('/v1/system/status/');
setStatus(data);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStatus();
const interval = setInterval(fetchStatus, 30000); // Refresh every 30 seconds
return () => clearInterval(interval);
}, []);
if (loading) {
return (
<>
<PageMeta title="System Status - IGNY8" description="System monitoring" />
<ComponentCard title="System Status" desc="Loading system information...">
<div className="text-center py-8">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-gray-900 dark:border-white mx-auto"></div>
</div>
</ComponentCard>
</>
);
}
if (error || !status) {
return (
<>
<PageMeta title="System Status - IGNY8" description="System monitoring" />
<ComponentCard title="System Status" desc="Error loading system information">
<div className="text-center py-8 text-red-600 dark:text-red-400">
{error || 'Failed to load system status'}
</div>
</ComponentCard>
</>
);
}
return (
<>
<PageMeta title="System Status - IGNY8" description="System monitoring" />
<div className="space-y-6">
{/* System Resources */}
<ComponentCard title="System Resources" desc="CPU, Memory, and Disk Usage">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* CPU */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">CPU</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.system?.cpu?.status || 'unknown')}`}>
{status.system?.cpu?.status || 'unknown'}
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-4">
<div
className={`h-4 rounded-full ${
(status.system?.cpu?.usage_percent || 0) < 80 ? 'bg-green-500' :
(status.system?.cpu?.usage_percent || 0) < 95 ? 'bg-yellow-500' : 'bg-red-500'
}`}
style={{ width: `${status.system?.cpu?.usage_percent || 0}%` }}
></div>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{status.system?.cpu?.usage_percent?.toFixed(1)}% used ({status.system?.cpu?.cores} cores)
</div>
</div>
{/* Memory */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Memory</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.system?.memory?.status || 'unknown')}`}>
{status.system?.memory?.status || 'unknown'}
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-4">
<div
className={`h-4 rounded-full ${
(status.system?.memory?.usage_percent || 0) < 80 ? 'bg-green-500' :
(status.system?.memory?.usage_percent || 0) < 95 ? 'bg-yellow-500' : 'bg-red-500'
}`}
style={{ width: `${status.system?.memory?.usage_percent || 0}%` }}
></div>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{status.system?.memory?.used_gb?.toFixed(1)} GB / {status.system?.memory?.total_gb?.toFixed(1)} GB
</div>
</div>
{/* Disk */}
<div className="space-y-2">
<div className="flex justify-between items-center">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">Disk</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.system?.disk?.status || 'unknown')}`}>
{status.system?.disk?.status || 'unknown'}
</span>
</div>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-4">
<div
className={`h-4 rounded-full ${
(status.system?.disk?.usage_percent || 0) < 80 ? 'bg-green-500' :
(status.system?.disk?.usage_percent || 0) < 95 ? 'bg-yellow-500' : 'bg-red-500'
}`}
style={{ width: `${status.system?.disk?.usage_percent || 0}%` }}
></div>
</div>
<div className="text-sm text-gray-600 dark:text-gray-400">
{status.system?.disk?.used_gb?.toFixed(1)} GB / {status.system?.disk?.total_gb?.toFixed(1)} GB
</div>
</div>
</div>
</ComponentCard>
{/* Services Status */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
{/* Database */}
<ComponentCard title="Database" desc="PostgreSQL Status">
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">Status</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.database?.status || 'unknown')}`}>
{status.database?.connected ? 'Connected' : 'Disconnected'}
</span>
</div>
{status.database?.version && (
<div className="text-sm">
<span className="text-gray-600 dark:text-gray-400">Version:</span>
<span className="ml-2 text-gray-800 dark:text-gray-200">{status.database.version.split(',')[0]}</span>
</div>
)}
{status.database?.size && (
<div className="text-sm">
<span className="text-gray-600 dark:text-gray-400">Size:</span>
<span className="ml-2 text-gray-800 dark:text-gray-200">{status.database.size}</span>
</div>
)}
<div className="text-sm">
<span className="text-gray-600 dark:text-gray-400">Active Connections:</span>
<span className="ml-2 text-gray-800 dark:text-gray-200">{status.database?.active_connections || 0}</span>
</div>
</div>
</ComponentCard>
{/* Redis */}
<ComponentCard title="Redis" desc="Cache & Message Broker">
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">Status</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.redis?.status || 'unknown')}`}>
{status.redis?.connected ? 'Connected' : 'Disconnected'}
</span>
</div>
</div>
</ComponentCard>
{/* Celery */}
<ComponentCard title="Celery" desc="Task Queue Workers">
<div className="space-y-3">
<div className="flex justify-between items-center">
<span className="text-sm text-gray-600 dark:text-gray-400">Workers</span>
<span className={`text-xs px-2 py-1 rounded ${getStatusBadge(status.celery?.status || 'unknown')}`}>
{status.celery?.worker_count || 0} active
</span>
</div>
<div className="text-sm">
<span className="text-gray-600 dark:text-gray-400">Active Tasks:</span>
<span className="ml-2 text-gray-800 dark:text-gray-200">{status.celery?.tasks?.active || 0}</span>
</div>
<div className="text-sm">
<span className="text-gray-600 dark:text-gray-400">Scheduled:</span>
<span className="ml-2 text-gray-800 dark:text-gray-200">{status.celery?.tasks?.scheduled || 0}</span>
</div>
<div className="text-sm">
<span className="text-gray-600 dark:text-gray-400">Reserved:</span>
<span className="ml-2 text-gray-800 dark:text-gray-200">{status.celery?.tasks?.reserved || 0}</span>
</div>
</div>
</ComponentCard>
</div>
{/* Process Monitoring by Stack */}
<ComponentCard title="Process Monitoring" desc="Resource usage by technology stack">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead>
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Stack</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Processes</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">CPU %</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Memory (MB)</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{Object.entries(status.processes?.by_stack || {}).map(([stack, stats]) => (
<tr key={stack}>
<td className="px-4 py-3 text-sm font-medium text-gray-800 dark:text-gray-200 capitalize">{stack}</td>
<td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">{stats.count}</td>
<td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">{stats.cpu.toFixed(2)}%</td>
<td className="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">{stats.memory_mb.toFixed(2)}</td>
</tr>
))}
</tbody>
</table>
</div>
</ComponentCard>
{/* Module Statistics */}
<ComponentCard title="Module Statistics" desc="Data counts by module">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Planner Module */}
<div className="space-y-3">
<h4 className="text-sm font-semibold text-gray-800 dark:text-gray-200">Planner Module</h4>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Keywords:</span>
<span className="text-gray-800 dark:text-gray-200">{status.modules?.planner?.keywords?.toLocaleString() || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Clusters:</span>
<span className="text-gray-800 dark:text-gray-200">{status.modules?.planner?.clusters?.toLocaleString() || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Content Ideas:</span>
<span className="text-gray-800 dark:text-gray-200">{status.modules?.planner?.content_ideas?.toLocaleString() || 0}</span>
</div>
</div>
</div>
{/* Writer Module */}
<div className="space-y-3">
<h4 className="text-sm font-semibold text-gray-800 dark:text-gray-200">Writer Module</h4>
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Tasks:</span>
<span className="text-gray-800 dark:text-gray-200">{status.modules?.writer?.tasks?.toLocaleString() || 0}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-gray-600 dark:text-gray-400">Images:</span>
<span className="text-gray-800 dark:text-gray-200">{status.modules?.writer?.images?.toLocaleString() || 0}</span>
</div>
</div>
</div>
</div>
</ComponentCard>
{/* Last Updated */}
<div className="text-center text-sm text-gray-500 dark:text-gray-400">
Last updated: {new Date(status.timestamp).toLocaleString()}
</div>
</div>
</>
);
}

View File

@@ -0,0 +1,99 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
interface Subscription {
id: number;
account_name: string;
status: string;
current_period_start: string;
current_period_end: string;
}
export default function Subscriptions() {
const toast = useToast();
const [subscriptions, setSubscriptions] = useState<Subscription[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadSubscriptions();
}, []);
const loadSubscriptions = async () => {
try {
setLoading(true);
const response = await fetchAPI('/v1/auth/subscriptions/');
setSubscriptions(response.results || []);
} catch (error: any) {
toast.error(`Failed to load subscriptions: ${error.message}`);
} finally {
setLoading(false);
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'active':
return 'success';
case 'past_due':
return 'warning';
case 'canceled':
return 'error';
default:
return 'primary';
}
};
return (
<div className="p-6">
<PageMeta title="Subscriptions" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Subscriptions</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Manage account subscriptions</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Account</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Status</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Period Start</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Period End</th>
</tr>
</thead>
<tbody>
{subscriptions.map((subscription) => (
<tr key={subscription.id} className="border-b border-gray-100 dark:border-gray-800">
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">{subscription.account_name}</td>
<td className="py-3 px-4">
<Badge variant="light" color={getStatusColor(subscription.status) as any}>
{subscription.status}
</Badge>
</td>
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">
{new Date(subscription.current_period_start).toLocaleDateString()}
</td>
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">
{new Date(subscription.current_period_end).toLocaleDateString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { Card } from '../../components/ui/card';
export default function SystemSettings() {
const toast = useToast();
const [settings, setSettings] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
try {
setLoading(true);
const response = await fetchAPI('/v1/system/settings/system/');
setSettings(response.results || []);
} catch (error: any) {
toast.error(`Failed to load system settings: ${error.message}`);
} finally {
setLoading(false);
}
};
return (
<div className="p-6">
<PageMeta title="System Settings" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">System Settings</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Global platform-wide settings</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<p className="text-gray-600 dark:text-gray-400">System settings management interface coming soon.</p>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,192 @@
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>
</>
);
}

View File

@@ -0,0 +1,121 @@
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>
</>
);
}

View File

@@ -0,0 +1,169 @@
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>
);
}

View File

@@ -0,0 +1,48 @@
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>
</>
);
}

View File

@@ -0,0 +1,116 @@
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>
);
}

View File

@@ -0,0 +1,89 @@
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>
</>
);
}

View File

@@ -0,0 +1,67 @@
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>
</>
);
}

View File

@@ -0,0 +1,21 @@
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>
</>
);
}

View File

@@ -0,0 +1,132 @@
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>
</>
);
}

View File

@@ -0,0 +1,27 @@
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>
</>
);
}

View File

@@ -0,0 +1,36 @@
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>
</>
);
}

View File

@@ -0,0 +1,46 @@
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>
</>
);
}

View File

@@ -0,0 +1,177 @@
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>
</>
);
}

View File

@@ -0,0 +1,278 @@
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>
</>
);
}

View File

@@ -0,0 +1,48 @@
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>
</>
);
}

View File

@@ -0,0 +1,21 @@
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>
</>
);
}

View File

@@ -0,0 +1,237 @@
import ComponentCard from "../../../components/common/ComponentCard";
import PageMeta from "../../../components/common/PageMeta";
import { PricingTable, PricingPlan } from "../../../components/ui/pricing-table";
// 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>
);
export default function PricingTablePage() {
// Sample plans for variant 1
const plans1: PricingPlan[] = [
{
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">
<PricingTable
variant="1"
title="Flexible Plans Tailored to Fit Your Unique Needs!"
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>
</>
);
}

View File

@@ -0,0 +1,66 @@
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>
</>
);
}

View File

@@ -0,0 +1,69 @@
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>
</>
);
}

View File

@@ -0,0 +1,74 @@
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>
</>
);
}

View File

@@ -0,0 +1,64 @@
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>
</>
);
}

View File

@@ -0,0 +1,34 @@
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>
</>
);
}

View File

@@ -0,0 +1,35 @@
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>
</>
);
}

View File

@@ -0,0 +1,84 @@
import { useState, useEffect } from 'react';
import PageMeta from '../../components/common/PageMeta';
import { useToast } from '../../components/ui/toast/ToastContainer';
import { fetchAPI } from '../../services/api';
import { Card } from '../../components/ui/card';
import Badge from '../../components/ui/badge/Badge';
interface User {
id: number;
email: string;
username: string;
role: string;
is_active: boolean;
}
export default function Users() {
const toast = useToast();
const [users, setUsers] = useState<User[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadUsers();
}, []);
const loadUsers = async () => {
try {
setLoading(true);
const response = await fetchAPI('/v1/auth/users/');
setUsers(response.results || []);
} catch (error: any) {
toast.error(`Failed to load users: ${error.message}`);
} finally {
setLoading(false);
}
};
return (
<div className="p-6">
<PageMeta title="Users" />
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Users</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1">Manage account users and permissions</p>
</div>
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading...</div>
</div>
) : (
<Card className="p-6">
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-700">
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Email</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Username</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Role</th>
<th className="text-left py-3 px-4 text-sm font-medium text-gray-700 dark:text-gray-300">Status</th>
</tr>
</thead>
<tbody>
{users.map((user) => (
<tr key={user.id} className="border-b border-gray-100 dark:border-gray-800">
<td className="py-3 px-4 text-sm text-gray-900 dark:text-white">{user.email}</td>
<td className="py-3 px-4 text-sm text-gray-600 dark:text-gray-400">{user.username}</td>
<td className="py-3 px-4">
<Badge variant="light" color="primary">{user.role}</Badge>
</td>
<td className="py-3 px-4">
<Badge variant="light" color={user.is_active ? 'success' : 'dark'}>
{user.is_active ? 'Active' : 'Inactive'}
</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
</Card>
)}
</div>
);
}