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:
poduck
2025-11-30 19:49:06 -05:00
parent 0d1a3045fb
commit 2b321aef57
34 changed files with 1930 additions and 1291 deletions

View File

@@ -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;

View 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;

View 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;

View 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;