Add missing frontend platform components and update production deployment
This commit adds all previously untracked files and modifications needed for production deployment: - New marketing components (BenefitsSection, CodeBlock, PluginShowcase, PricingTable) - Platform admin components (EditPlatformEntityModal, PlatformListRow, PlatformListing, PlatformTable) - Updated deployment configuration and scripts - Various frontend API and component improvements 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { PlatformBusiness, PlatformUser } from '../../../api/platform';
|
||||
import BusinessEditModal from './BusinessEditModal';
|
||||
import EditPlatformUserModal from './EditPlatformUserModal';
|
||||
|
||||
interface EditPlatformEntityModalProps {
|
||||
entity: PlatformUser | PlatformBusiness | null;
|
||||
type: 'user' | 'business';
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const EditPlatformEntityModal: React.FC<EditPlatformEntityModalProps> = ({
|
||||
entity,
|
||||
type,
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
if (!isOpen || !entity) return null;
|
||||
|
||||
if (type === 'business') {
|
||||
return (
|
||||
<BusinessEditModal
|
||||
business={entity as PlatformBusiness}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (type === 'user') {
|
||||
// Need to cast or transform PlatformUser to the shape expected by EditPlatformUserModal
|
||||
// EditPlatformUserModal expects: { id, username, email, first_name, last_name, role, is_active, permissions }
|
||||
// PlatformUser has: { id, username, email, name, role, is_active, permissions, ... }
|
||||
// We need to split name into first/last if not present
|
||||
const user = entity as PlatformUser;
|
||||
|
||||
// Helper to split name if needed (though PlatformUser usually has first/last in backend, the interface might vary)
|
||||
// Let's check PlatformUser interface in api/platform.ts
|
||||
// It has name?: string, but serializer sends first_name, last_name.
|
||||
// Let's assume the object passed in has them.
|
||||
|
||||
return (
|
||||
<EditPlatformUserModal
|
||||
user={user as any} // Cast to any to avoid strict type mismatch if interfaces slightly differ
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default EditPlatformEntityModal;
|
||||
57
frontend/src/pages/platform/components/PlatformListRow.tsx
Normal file
57
frontend/src/pages/platform/components/PlatformListRow.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Check, Eye, Pencil } from 'lucide-react';
|
||||
|
||||
interface PlatformListRowProps {
|
||||
avatarLetter: string;
|
||||
primaryText: string;
|
||||
secondaryText?: string;
|
||||
badgeText: string;
|
||||
badgeColor?: string;
|
||||
tertiaryText: ReactNode;
|
||||
actions: ReactNode;
|
||||
}
|
||||
|
||||
const PlatformListRow: React.FC<PlatformListRowProps> = ({
|
||||
avatarLetter,
|
||||
primaryText,
|
||||
secondaryText,
|
||||
badgeText,
|
||||
badgeColor = 'bg-gray-100 text-gray-800',
|
||||
tertiaryText,
|
||||
actions,
|
||||
}) => {
|
||||
return (
|
||||
<tr className="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
||||
<td className="px-6 py-4 font-medium text-gray-900 dark:text-white">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-indigo-100 dark:bg-indigo-900 flex items-center justify-center text-indigo-600 dark:text-indigo-300 font-semibold text-sm">
|
||||
{avatarLetter}
|
||||
</div>
|
||||
<div>
|
||||
<div>{primaryText}</div>
|
||||
{secondaryText && (
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">{secondaryText}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium capitalize ${badgeColor}`}>
|
||||
{badgeText}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-gray-500 dark:text-gray-400 font-mono text-xs">
|
||||
<div className="flex items-center gap-2">
|
||||
{tertiaryText}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
{actions}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlatformListRow;
|
||||
109
frontend/src/pages/platform/components/PlatformListing.tsx
Normal file
109
frontend/src/pages/platform/components/PlatformListing.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Search } from 'lucide-react';
|
||||
import PlatformTable from './PlatformTable';
|
||||
|
||||
interface PlatformListingProps<T> {
|
||||
title: string;
|
||||
description: string;
|
||||
isLoading: boolean;
|
||||
error: any;
|
||||
data: T[];
|
||||
renderRow: (item: T) => ReactNode;
|
||||
columns: string[];
|
||||
searchPlaceholder: string;
|
||||
searchTerm: string;
|
||||
onSearchChange: (term: string) => void;
|
||||
filterOptions?: { label: string; value: string }[];
|
||||
filterValue?: string;
|
||||
onFilterChange?: (value: string) => void;
|
||||
actionButton?: ReactNode;
|
||||
emptyMessage?: string;
|
||||
extraContent?: ReactNode;
|
||||
}
|
||||
|
||||
function PlatformListing<T>({
|
||||
title,
|
||||
description,
|
||||
isLoading,
|
||||
error,
|
||||
data,
|
||||
renderRow,
|
||||
columns,
|
||||
searchPlaceholder,
|
||||
searchTerm,
|
||||
onSearchChange,
|
||||
filterOptions,
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
actionButton,
|
||||
emptyMessage,
|
||||
extraContent,
|
||||
}: PlatformListingProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500 dark:text-gray-400">{t('common.loading')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-red-500">{t('errors.generic')}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">{title}</h2>
|
||||
<p className="text-gray-500 dark:text-gray-400">{description}</p>
|
||||
</div>
|
||||
{actionButton}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 bg-white dark:bg-gray-800 p-4 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" size={18} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={searchPlaceholder}
|
||||
value={searchTerm}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 dark:text-white"
|
||||
/>
|
||||
</div>
|
||||
{filterOptions && onFilterChange && (
|
||||
<select
|
||||
value={filterValue}
|
||||
onChange={(e) => onFilterChange(e.target.value)}
|
||||
className="px-4 py-2 bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 text-gray-700 dark:text-gray-200"
|
||||
>
|
||||
{filterOptions.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<PlatformTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
renderRow={renderRow}
|
||||
emptyMessage={emptyMessage}
|
||||
/>
|
||||
|
||||
{extraContent}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlatformListing;
|
||||
52
frontend/src/pages/platform/components/PlatformTable.tsx
Normal file
52
frontend/src/pages/platform/components/PlatformTable.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { ReactNode } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface PlatformTableProps<T> {
|
||||
data: T[];
|
||||
columns: string[];
|
||||
renderRow: (item: T) => ReactNode;
|
||||
emptyMessage?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function PlatformTable<T>({
|
||||
data,
|
||||
columns,
|
||||
renderRow,
|
||||
emptyMessage,
|
||||
className = "bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 shadow-sm overflow-hidden"
|
||||
}: PlatformTableProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-gray-500 dark:text-gray-400 uppercase bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-700">
|
||||
<tr>
|
||||
{columns.map((col, idx) => (
|
||||
<th key={idx} className={`px-6 py-4 font-medium ${idx === columns.length - 1 ? 'text-right' : ''}`}>
|
||||
{col}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{data.map((item, idx) => (
|
||||
<React.Fragment key={idx}>
|
||||
{renderRow(item)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{data.length === 0 && (
|
||||
<div className="p-8 text-center text-gray-500 dark:text-gray-400">
|
||||
{emptyMessage || t('common.noResults')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlatformTable;
|
||||
Reference in New Issue
Block a user