fix: Remove Intelligence Library components causing build failure
- Remove incomplete CRUD components that require Shadcn UI - Keep Jumpstart fix and frontend plugin upgrades - Diagnostic test confirms all API connections working - Build now succeeds and ready for deployment Note: Intelligence Library UI components will be added in next phase after Shadcn UI components are properly set up.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1,369 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { getDirectusClient, readItems, createItem, updateItem, deleteItem } from '@/lib/directus/client';
|
||||
import { DataTable } from '../shared/DataTable';
|
||||
import { CRUDModal } from '../shared/CRUDModal';
|
||||
import { DeleteConfirm } from '../shared/DeleteConfirm';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
// Validation schema
|
||||
const avatarVariantSchema = z.object({
|
||||
avatar_key: z.string().min(1, 'Avatar key is required'),
|
||||
variant_type: z.enum(['male', 'female', 'neutral']),
|
||||
pronoun: z.string().min(1, 'Pronoun is required'),
|
||||
identity: z.string().min(1, 'Identity is required'),
|
||||
tone_modifiers: z.string().optional(),
|
||||
});
|
||||
|
||||
type AvatarVariantFormData = z.infer<typeof avatarVariantSchema>;
|
||||
|
||||
interface AvatarVariant {
|
||||
id: string;
|
||||
avatar_key: string;
|
||||
variant_type: 'male' | 'female' | 'neutral';
|
||||
pronoun: string;
|
||||
identity: string;
|
||||
tone_modifiers?: string;
|
||||
}
|
||||
|
||||
export default function AvatarVariantManager() {
|
||||
const [variants, setVariants] = useState<AvatarVariant[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [editingVariant, setEditingVariant] = useState<AvatarVariant | null>(null);
|
||||
const [deletingVariant, setDeletingVariant] = useState<AvatarVariant | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<AvatarVariantFormData>({
|
||||
resolver: zodResolver(avatarVariantSchema),
|
||||
});
|
||||
|
||||
const variantType = watch('variant_type');
|
||||
|
||||
// Load data
|
||||
const loadVariants = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const client = getDirectusClient();
|
||||
const data = await client.request(
|
||||
readItems('avatar_variants', {
|
||||
fields: ['*'],
|
||||
sort: ['avatar_key', 'variant_type'],
|
||||
})
|
||||
);
|
||||
setVariants(data as AvatarVariant[]);
|
||||
} catch (error) {
|
||||
console.error('Error loading avatar variants:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadVariants();
|
||||
}, []);
|
||||
|
||||
// Handle create/edit
|
||||
const onSubmit = async (data: AvatarVariantFormData) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const client = getDirectusClient();
|
||||
|
||||
if (editingVariant) {
|
||||
await client.request(
|
||||
updateItem('avatar_variants', editingVariant.id, data)
|
||||
);
|
||||
} else {
|
||||
await client.request(createItem('avatar_variants', data));
|
||||
}
|
||||
|
||||
await loadVariants();
|
||||
setIsModalOpen(false);
|
||||
reset();
|
||||
setEditingVariant(null);
|
||||
} catch (error) {
|
||||
console.error('Error saving variant:', error);
|
||||
alert('Failed to save variant');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle delete
|
||||
const handleDelete = async () => {
|
||||
if (!deletingVariant) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const client = getDirectusClient();
|
||||
await client.request(deleteItem('avatar_variants', deletingVariant.id));
|
||||
await loadVariants();
|
||||
setIsDeleteOpen(false);
|
||||
setDeletingVariant(null);
|
||||
} catch (error) {
|
||||
console.error('Error deleting variant:', error);
|
||||
alert('Failed to delete variant');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle edit click
|
||||
const handleEdit = (variant: AvatarVariant) => {
|
||||
setEditingVariant(variant);
|
||||
setValue('avatar_key', variant.avatar_key);
|
||||
setValue('variant_type', variant.variant_type);
|
||||
setValue('pronoun', variant.pronoun);
|
||||
setValue('identity', variant.identity);
|
||||
setValue('tone_modifiers', variant.tone_modifiers || '');
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// Handle add click
|
||||
const handleAdd = () => {
|
||||
setEditingVariant(null);
|
||||
reset();
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// Handle delete click
|
||||
const handleDeleteClick = (variant: AvatarVariant) => {
|
||||
setDeletingVariant(variant);
|
||||
setIsDeleteOpen(true);
|
||||
};
|
||||
|
||||
// Export data
|
||||
const handleExport = () => {
|
||||
const dataStr = JSON.stringify(variants, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `avatar-variants-${new Date().toISOString().split('T')[0]}.json`;
|
||||
link.click();
|
||||
};
|
||||
|
||||
// Table columns
|
||||
const columns: ColumnDef<AvatarVariant>[] = [
|
||||
{
|
||||
accessorKey: 'avatar_key',
|
||||
header: 'Avatar',
|
||||
cell: ({ row }) => (
|
||||
<span className="font-medium text-white">{row.original.avatar_key}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'variant_type',
|
||||
header: 'Type',
|
||||
cell: ({ row }) => {
|
||||
const type = row.original.variant_type;
|
||||
const colors = {
|
||||
male: 'bg-blue-500/20 text-blue-400',
|
||||
female: 'bg-pink-500/20 text-pink-400',
|
||||
neutral: 'bg-purple-500/20 text-purple-400',
|
||||
};
|
||||
return (
|
||||
<Badge className={colors[type]}>
|
||||
{type}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'pronoun',
|
||||
header: 'Pronoun',
|
||||
},
|
||||
{
|
||||
accessorKey: 'identity',
|
||||
header: 'Identity',
|
||||
},
|
||||
{
|
||||
accessorKey: 'tone_modifiers',
|
||||
header: 'Tone Modifiers',
|
||||
cell: ({ row }) => row.original.tone_modifiers || '—',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(row.original)}
|
||||
className="text-blue-400 hover:text-blue-300 hover:bg-blue-500/10"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteClick(row.original)}
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">Total Variants</div>
|
||||
<div className="text-3xl font-bold text-white">{variants.length}</div>
|
||||
</div>
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">Male</div>
|
||||
<div className="text-3xl font-bold text-blue-400">
|
||||
{variants.filter((v) => v.variant_type === 'male').length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">Female</div>
|
||||
<div className="text-3xl font-bold text-pink-400">
|
||||
{variants.filter((v) => v.variant_type === 'female').length}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">Neutral</div>
|
||||
<div className="text-3xl font-bold text-purple-400">
|
||||
{variants.filter((v) => v.variant_type === 'neutral').length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Table */}
|
||||
<DataTable
|
||||
data={variants}
|
||||
columns={columns}
|
||||
onAdd={handleAdd}
|
||||
onExport={handleExport}
|
||||
searchPlaceholder="Search variants..."
|
||||
addButtonText="Add Variant"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<CRUDModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setEditingVariant(null);
|
||||
reset();
|
||||
}}
|
||||
title={editingVariant ? 'Edit Avatar Variant' : 'Create Avatar Variant'}
|
||||
description="Configure gender and tone variations for an avatar"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
isSubmitting={isSubmitting}
|
||||
>
|
||||
<form className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="avatar_key">Avatar Key</Label>
|
||||
<Input
|
||||
id="avatar_key"
|
||||
{...register('avatar_key')}
|
||||
placeholder="e.g., dr_smith"
|
||||
className="bg-slate-900 border-slate-700"
|
||||
/>
|
||||
{errors.avatar_key && (
|
||||
<p className="text-red-400 text-sm mt-1">{errors.avatar_key.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="variant_type">Variant Type</Label>
|
||||
<Select
|
||||
value={variantType}
|
||||
onValueChange={(value) => setValue('variant_type', value as any)}
|
||||
>
|
||||
<SelectTrigger className="bg-slate-900 border-slate-700">
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="bg-slate-800 border-slate-700">
|
||||
<SelectItem value="male">Male</SelectItem>
|
||||
<SelectItem value="female">Female</SelectItem>
|
||||
<SelectItem value="neutral">Neutral</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{errors.variant_type && (
|
||||
<p className="text-red-400 text-sm mt-1">{errors.variant_type.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="pronoun">Pronoun</Label>
|
||||
<Input
|
||||
id="pronoun"
|
||||
{...register('pronoun')}
|
||||
placeholder="e.g., he/him, she/her, they/them"
|
||||
className="bg-slate-900 border-slate-700"
|
||||
/>
|
||||
{errors.pronoun && (
|
||||
<p className="text-red-400 text-sm mt-1">{errors.pronoun.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="identity">Identity</Label>
|
||||
<Input
|
||||
id="identity"
|
||||
{...register('identity')}
|
||||
placeholder="e.g., Dr. Sarah Smith"
|
||||
className="bg-slate-900 border-slate-700"
|
||||
/>
|
||||
{errors.identity && (
|
||||
<p className="text-red-400 text-sm mt-1">{errors.identity.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="tone_modifiers">Tone Modifiers (Optional)</Label>
|
||||
<Input
|
||||
id="tone_modifiers"
|
||||
{...register('tone_modifiers')}
|
||||
placeholder="e.g., professional, friendly"
|
||||
className="bg-slate-900 border-slate-700"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</CRUDModal>
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<DeleteConfirm
|
||||
isOpen={isDeleteOpen}
|
||||
onClose={() => {
|
||||
setIsDeleteOpen(false);
|
||||
setDeletingVariant(null);
|
||||
}}
|
||||
onConfirm={handleDelete}
|
||||
itemName={deletingVariant?.identity}
|
||||
isDeleting={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,155 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { getDirectusClient, readItems } from '@/lib/directus/client';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
interface GeoLocation {
|
||||
id: string;
|
||||
city: string;
|
||||
state: string;
|
||||
zip_focus?: string;
|
||||
neighborhood?: string;
|
||||
cluster: number;
|
||||
}
|
||||
|
||||
interface GeoCluster {
|
||||
id: number;
|
||||
cluster_name: string;
|
||||
locations?: GeoLocation[];
|
||||
}
|
||||
|
||||
export default function GeoIntelligenceManager() {
|
||||
const [clusters, setClusters] = useState<GeoCluster[]>([]);
|
||||
const [locations, setLocations] = useState<GeoLocation[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Load data
|
||||
const loadData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const client = getDirectusClient();
|
||||
|
||||
// Load clusters
|
||||
const clustersData = await client.request(
|
||||
readItems('geo_clusters', {
|
||||
fields: ['*'],
|
||||
sort: ['cluster_name'],
|
||||
})
|
||||
);
|
||||
|
||||
// Load locations
|
||||
const locationsData = await client.request(
|
||||
readItems('geo_locations', {
|
||||
fields: ['*'],
|
||||
sort: ['state', 'city'],
|
||||
})
|
||||
);
|
||||
|
||||
setClusters(clustersData as GeoCluster[]);
|
||||
setLocations(locationsData as GeoLocation[]);
|
||||
} catch (error) {
|
||||
console.error('Error loading geo intelligence:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
// Group locations by cluster
|
||||
const locationsByCluster = locations.reduce((acc, loc) => {
|
||||
if (!acc[loc.cluster]) acc[loc.cluster] = [];
|
||||
acc[loc.cluster].push(loc);
|
||||
return acc;
|
||||
}, {} as Record<number, GeoLocation[]>);
|
||||
|
||||
const totalCities = locations.length;
|
||||
const totalStates = new Set(locations.map(l => l.state)).size;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">Total Clusters</div>
|
||||
<div className="text-3xl font-bold text-white">{clusters.length}</div>
|
||||
</div>
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">Total Cities</div>
|
||||
<div className="text-3xl font-bold text-blue-400">{totalCities}</div>
|
||||
</div>
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">States Covered</div>
|
||||
<div className="text-3xl font-bold text-green-400">{totalStates}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Clusters */}
|
||||
<div className="space-y-4">
|
||||
{clusters.map((cluster) => {
|
||||
const clusterLocations = locationsByCluster[cluster.id] || [];
|
||||
|
||||
return (
|
||||
<Card key={cluster.id} className="bg-slate-800 border-slate-700">
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="text-white flex justify-between items-center">
|
||||
<span className="flex items-center gap-3">
|
||||
🗺️ {cluster.cluster_name}
|
||||
</span>
|
||||
<Badge className="bg-blue-600">
|
||||
{clusterLocations.length} Cities
|
||||
</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
|
||||
{clusterLocations.map((loc) => (
|
||||
<div
|
||||
key={loc.id}
|
||||
className="bg-slate-900 border border-slate-700 rounded-lg p-4 hover:border-blue-500/50 transition-colors"
|
||||
>
|
||||
<div className="font-medium text-white mb-1">
|
||||
{loc.city}, {loc.state}
|
||||
</div>
|
||||
{loc.neighborhood && (
|
||||
<div className="text-xs text-slate-400 mb-1">
|
||||
📍 {loc.neighborhood}
|
||||
</div>
|
||||
)}
|
||||
{loc.zip_focus && (
|
||||
<div className="text-xs text-slate-500">
|
||||
ZIP: {loc.zip_focus}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{clusters.length === 0 && (
|
||||
<Card className="bg-slate-800 border-slate-700">
|
||||
<CardContent className="p-12 text-center">
|
||||
<p className="text-slate-400 mb-4">No geographic clusters found.</p>
|
||||
<p className="text-sm text-slate-500">
|
||||
Run the schema initialization script to import geo intelligence data.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { getDirectusClient, readItems, createItem, updateItem, deleteItem } from '@/lib/directus/client';
|
||||
import { DataTable } from '../shared/DataTable';
|
||||
import { CRUDModal } from '../shared/CRUDModal';
|
||||
import { DeleteConfirm } from '../shared/DeleteConfirm';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
// Validation schema
|
||||
const cartesianSchema = z.object({
|
||||
pattern_key: z.string().min(1, 'Pattern key is required'),
|
||||
pattern_type: z.string().min(1, 'Pattern type is required'),
|
||||
formula: z.string().min(1, 'Formula is required'),
|
||||
example_output: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
type CartesianFormData = z.infer<typeof cartesianSchema>;
|
||||
|
||||
interface CartesianPattern {
|
||||
id: string;
|
||||
pattern_key: string;
|
||||
pattern_type: string;
|
||||
formula: string;
|
||||
example_output?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function CartesianManager() {
|
||||
const [patterns, setPatterns] = useState<CartesianPattern[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [editingPattern, setEditingPattern] = useState<CartesianPattern | null>(null);
|
||||
const [deletingPattern, setDeletingPattern] = useState<CartesianPattern | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<CartesianFormData>({
|
||||
resolver: zodResolver(cartesianSchema),
|
||||
});
|
||||
|
||||
// Load data
|
||||
const loadPatterns = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const client = getDirectusClient();
|
||||
const data = await client.request(
|
||||
readItems('cartesian_patterns', {
|
||||
fields: ['*'],
|
||||
sort: ['pattern_type', 'pattern_key'],
|
||||
})
|
||||
);
|
||||
setPatterns(data as CartesianPattern[]);
|
||||
} catch (error) {
|
||||
console.error('Error loading cartesian patterns:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadPatterns();
|
||||
}, []);
|
||||
|
||||
// Handle create/edit
|
||||
const onSubmit = async (data: CartesianFormData) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const client = getDirectusClient();
|
||||
|
||||
if (editingPattern) {
|
||||
await client.request(
|
||||
updateItem('cartesian_patterns', editingPattern.id, data)
|
||||
);
|
||||
} else {
|
||||
await client.request(createItem('cartesian_patterns', data));
|
||||
}
|
||||
|
||||
await loadPatterns();
|
||||
setIsModalOpen(false);
|
||||
reset();
|
||||
setEditingPattern(null);
|
||||
} catch (error) {
|
||||
console.error('Error saving pattern:', error);
|
||||
alert('Failed to save pattern');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle delete
|
||||
const handleDelete = async () => {
|
||||
if (!deletingPattern) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const client = getDirectusClient();
|
||||
await client.request(deleteItem('cartesian_patterns', deletingPattern.id));
|
||||
await loadPatterns();
|
||||
setIsDeleteOpen(false);
|
||||
setDeletingPattern(null);
|
||||
} catch (error) {
|
||||
console.error('Error deleting pattern:', error);
|
||||
alert('Failed to delete pattern');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle edit click
|
||||
const handleEdit = (pattern: CartesianPattern) => {
|
||||
setEditingPattern(pattern);
|
||||
Object.keys(pattern).forEach((key) => {
|
||||
setValue(key as any, (pattern as any)[key]);
|
||||
});
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// Handle add click
|
||||
const handleAdd = () => {
|
||||
setEditingPattern(null);
|
||||
reset();
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// Handle delete click
|
||||
const handleDeleteClick = (pattern: CartesianPattern) => {
|
||||
setDeletingPattern(pattern);
|
||||
setIsDeleteOpen(true);
|
||||
};
|
||||
|
||||
// Export data
|
||||
const handleExport = () => {
|
||||
const dataStr = JSON.stringify(patterns, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `cartesian-patterns-${new Date().toISOString().split('T')[0]}.json`;
|
||||
link.click();
|
||||
};
|
||||
|
||||
// Table columns
|
||||
const columns: ColumnDef<CartesianPattern>[] = [
|
||||
{
|
||||
accessorKey: 'pattern_key',
|
||||
header: 'Pattern Key',
|
||||
cell: ({ row }) => (
|
||||
<span className="font-medium text-white font-mono">{row.original.pattern_key}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'pattern_type',
|
||||
header: 'Type',
|
||||
cell: ({ row }) => (
|
||||
<Badge className="bg-purple-600">
|
||||
{row.original.pattern_type}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'formula',
|
||||
header: 'Formula',
|
||||
cell: ({ row }) => (
|
||||
<code className="text-xs text-green-400 bg-slate-900 px-2 py-1 rounded">
|
||||
{row.original.formula.length > 50
|
||||
? row.original.formula.substring(0, 50) + '...'
|
||||
: row.original.formula}
|
||||
</code>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'example_output',
|
||||
header: 'Example',
|
||||
cell: ({ row }) => (
|
||||
<span className="text-sm italic text-slate-400">
|
||||
{row.original.example_output
|
||||
? (row.original.example_output.length > 40
|
||||
? row.original.example_output.substring(0, 40) + '...'
|
||||
: row.original.example_output)
|
||||
: '—'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(row.original)}
|
||||
className="text-blue-400 hover:text-blue-300 hover:bg-blue-500/10"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteClick(row.original)}
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const patternTypes = new Set(patterns.map(p => p.pattern_type));
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">Total Patterns</div>
|
||||
<div className="text-3xl font-bold text-white">{patterns.length}</div>
|
||||
</div>
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">Pattern Types</div>
|
||||
<div className="text-3xl font-bold text-purple-400">{patternTypes.size}</div>
|
||||
</div>
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">Avg Formula Length</div>
|
||||
<div className="text-3xl font-bold text-green-400">
|
||||
{patterns.length > 0
|
||||
? Math.round(patterns.reduce((sum, p) => sum + p.formula.length, 0) / patterns.length)
|
||||
: 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Table */}
|
||||
<DataTable
|
||||
data={patterns}
|
||||
columns={columns}
|
||||
onAdd={handleAdd}
|
||||
onExport={handleExport}
|
||||
searchPlaceholder="Search patterns..."
|
||||
addButtonText="Add Pattern"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<CRUDModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setEditingPattern(null);
|
||||
reset();
|
||||
}}
|
||||
title={editingPattern ? 'Edit Cartesian Pattern' : 'Create Cartesian Pattern'}
|
||||
description="Define content generation patterns using Cartesian product logic"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
isSubmitting={isSubmitting}
|
||||
>
|
||||
<form className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="pattern_key">Pattern Key</Label>
|
||||
<Input
|
||||
id="pattern_key"
|
||||
{...register('pattern_key')}
|
||||
placeholder="e.g., headline_template_1"
|
||||
className="bg-slate-900 border-slate-700"
|
||||
/>
|
||||
{errors.pattern_key && (
|
||||
<p className="text-red-400 text-sm mt-1">{errors.pattern_key.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="pattern_type">Pattern Type</Label>
|
||||
<Input
|
||||
id="pattern_type"
|
||||
{...register('pattern_type')}
|
||||
placeholder="e.g., headline, intro, cta"
|
||||
className="bg-slate-900 border-slate-700"
|
||||
/>
|
||||
{errors.pattern_type && (
|
||||
<p className="text-red-400 text-sm mt-1">{errors.pattern_type.message}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="formula">Formula</Label>
|
||||
<Textarea
|
||||
id="formula"
|
||||
{...register('formula')}
|
||||
placeholder="e.g., {adjective} {noun} in {location}"
|
||||
className="bg-slate-900 border-slate-700 font-mono text-sm"
|
||||
rows={3}
|
||||
/>
|
||||
{errors.formula && (
|
||||
<p className="text-red-400 text-sm mt-1">{errors.formula.message}</p>
|
||||
)}
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Use {'{'}curly braces{'}'} for variables that will be replaced
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="example_output">Example Output (Optional)</Label>
|
||||
<Textarea
|
||||
id="example_output"
|
||||
{...register('example_output')}
|
||||
placeholder="e.g., Amazing Services in Austin"
|
||||
className="bg-slate-900 border-slate-700"
|
||||
rows={2}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">Description (Optional)</Label>
|
||||
<Input
|
||||
id="description"
|
||||
{...register('description')}
|
||||
placeholder="e.g., Main headline template for service pages"
|
||||
className="bg-slate-900 border-slate-700"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</CRUDModal>
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<DeleteConfirm
|
||||
isOpen={isDeleteOpen}
|
||||
onClose={() => {
|
||||
setIsDeleteOpen(false);
|
||||
setDeletingPattern(null);
|
||||
}}
|
||||
onConfirm={handleDelete}
|
||||
itemName={deletingPattern?.pattern_key}
|
||||
isDeleting={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,333 +0,0 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { z } from 'zod';
|
||||
import type { ColumnDef } from '@tanstack/react-table';
|
||||
import { getDirectusClient, readItems, createItem, updateItem, deleteItem } from '@/lib/directus/client';
|
||||
import { DataTable } from '../shared/DataTable';
|
||||
import { CRUDModal } from '../shared/CRUDModal';
|
||||
import { DeleteConfirm } from '../shared/DeleteConfirm';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
// Validation schema
|
||||
const spintaxSchema = z.object({
|
||||
category: z.string().min(1, 'Category is required'),
|
||||
terms: z.string().min(1, 'At least one term is required'),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
type SpintaxFormData = z.infer<typeof spintaxSchema>;
|
||||
|
||||
interface SpintaxDictionary {
|
||||
id: string;
|
||||
category: string;
|
||||
data: string[];
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export default function SpintaxManager() {
|
||||
const [dictionaries, setDictionaries] = useState<SpintaxDictionary[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
const [editingDict, setEditingDict] = useState<SpintaxDictionary | null>(null);
|
||||
const [deletingDict, setDeletingDict] = useState<SpintaxDictionary | null>(null);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
setValue,
|
||||
formState: { errors },
|
||||
} = useForm<SpintaxFormData>({
|
||||
resolver: zodResolver(spintaxSchema),
|
||||
});
|
||||
|
||||
// Load data
|
||||
const loadDictionaries = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const client = getDirectusClient();
|
||||
const data = await client.request(
|
||||
readItems('spintax_dictionaries', {
|
||||
fields: ['*'],
|
||||
sort: ['category'],
|
||||
})
|
||||
);
|
||||
setDictionaries(data as SpintaxDictionary[]);
|
||||
} catch (error) {
|
||||
console.error('Error loading spintax dictionaries:', error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadDictionaries();
|
||||
}, []);
|
||||
|
||||
// Handle create/edit
|
||||
const onSubmit = async (formData: SpintaxFormData) => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const client = getDirectusClient();
|
||||
|
||||
// Convert comma-separated terms to array
|
||||
const termsArray = formData.terms
|
||||
.split(',')
|
||||
.map(t => t.trim())
|
||||
.filter(t => t.length > 0);
|
||||
|
||||
const data = {
|
||||
category: formData.category,
|
||||
data: termsArray,
|
||||
description: formData.description,
|
||||
};
|
||||
|
||||
if (editingDict) {
|
||||
await client.request(
|
||||
updateItem('spintax_dictionaries', editingDict.id, data)
|
||||
);
|
||||
} else {
|
||||
await client.request(createItem('spintax_dictionaries', data));
|
||||
}
|
||||
|
||||
await loadDictionaries();
|
||||
setIsModalOpen(false);
|
||||
reset();
|
||||
setEditingDict(null);
|
||||
} catch (error) {
|
||||
console.error('Error saving dictionary:', error);
|
||||
alert('Failed to save dictionary');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle delete
|
||||
const handleDelete = async () => {
|
||||
if (!deletingDict) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
const client = getDirectusClient();
|
||||
await client.request(deleteItem('spintax_dictionaries', deletingDict.id));
|
||||
await loadDictionaries();
|
||||
setIsDeleteOpen(false);
|
||||
setDeletingDict(null);
|
||||
} catch (error) {
|
||||
console.error('Error deleting dictionary:', error);
|
||||
alert('Failed to delete dictionary');
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle edit click
|
||||
const handleEdit = (dict: SpintaxDictionary) => {
|
||||
setEditingDict(dict);
|
||||
setValue('category', dict.category);
|
||||
setValue('terms', (dict.data || []).join(', '));
|
||||
setValue('description', dict.description || '');
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// Handle add click
|
||||
const handleAdd = () => {
|
||||
setEditingDict(null);
|
||||
reset();
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
// Handle delete click
|
||||
const handleDeleteClick = (dict: SpintaxDictionary) => {
|
||||
setDeletingDict(dict);
|
||||
setIsDeleteOpen(true);
|
||||
};
|
||||
|
||||
// Export data
|
||||
const handleExport = () => {
|
||||
const dataStr = JSON.stringify(dictionaries, null, 2);
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(dataBlob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `spintax-dictionaries-${new Date().toISOString().split('T')[0]}.json`;
|
||||
link.click();
|
||||
};
|
||||
|
||||
// Table columns
|
||||
const columns: ColumnDef<SpintaxDictionary>[] = [
|
||||
{
|
||||
accessorKey: 'category',
|
||||
header: 'Category',
|
||||
cell: ({ row }) => (
|
||||
<span className="font-medium text-white">{row.original.category}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'data',
|
||||
header: 'Terms',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex flex-wrap gap-1 max-w-md">
|
||||
{(row.original.data || []).slice(0, 5).map((term, i) => (
|
||||
<Badge key={i} variant="outline" className="text-xs">
|
||||
{term}
|
||||
</Badge>
|
||||
))}
|
||||
{(row.original.data || []).length > 5 && (
|
||||
<Badge variant="outline" className="text-xs text-slate-500">
|
||||
+{(row.original.data || []).length - 5} more
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'count',
|
||||
header: 'Count',
|
||||
cell: ({ row }) => (
|
||||
<Badge className="bg-blue-600">
|
||||
{(row.original.data || []).length}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: 'description',
|
||||
header: 'Description',
|
||||
cell: ({ row }) => row.original.description || '—',
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: 'Actions',
|
||||
cell: ({ row }) => (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(row.original)}
|
||||
className="text-blue-400 hover:text-blue-300 hover:bg-blue-500/10"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteClick(row.original)}
|
||||
className="text-red-400 hover:text-red-300 hover:bg-red-500/10"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const totalTerms = dictionaries.reduce((sum, d) => sum + (d.data || []).length, 0);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">Total Dictionaries</div>
|
||||
<div className="text-3xl font-bold text-white">{dictionaries.length}</div>
|
||||
</div>
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">Total Terms</div>
|
||||
<div className="text-3xl font-bold text-blue-400">{totalTerms}</div>
|
||||
</div>
|
||||
<div className="bg-slate-800 border border-slate-700 rounded-lg p-6">
|
||||
<div className="text-sm text-slate-400 mb-2">Avg Terms/Dict</div>
|
||||
<div className="text-3xl font-bold text-green-400">
|
||||
{dictionaries.length > 0 ? Math.round(totalTerms / dictionaries.length) : 0}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Table */}
|
||||
<DataTable
|
||||
data={dictionaries}
|
||||
columns={columns}
|
||||
onAdd={handleAdd}
|
||||
onExport={handleExport}
|
||||
searchPlaceholder="Search dictionaries..."
|
||||
addButtonText="Add Dictionary"
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<CRUDModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false);
|
||||
setEditingDict(null);
|
||||
reset();
|
||||
}}
|
||||
title={editingDict ? 'Edit Spintax Dictionary' : 'Create Spintax Dictionary'}
|
||||
description="Manage synonym variations for content generation"
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
isSubmitting={isSubmitting}
|
||||
>
|
||||
<form className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="category">Category</Label>
|
||||
<Input
|
||||
id="category"
|
||||
{...register('category')}
|
||||
placeholder="e.g., adjectives, verbs, locations"
|
||||
className="bg-slate-900 border-slate-700"
|
||||
/>
|
||||
{errors.category && (
|
||||
<p className="text-red-400 text-sm mt-1">{errors.category.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="terms">Terms (comma-separated)</Label>
|
||||
<Textarea
|
||||
id="terms"
|
||||
{...register('terms')}
|
||||
placeholder="e.g., great, excellent, amazing, fantastic"
|
||||
className="bg-slate-900 border-slate-700"
|
||||
rows={4}
|
||||
/>
|
||||
{errors.terms && (
|
||||
<p className="text-red-400 text-sm mt-1">{errors.terms.message}</p>
|
||||
)}
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Separate terms with commas. Each term will be a variation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="description">Description (Optional)</Label>
|
||||
<Input
|
||||
id="description"
|
||||
{...register('description')}
|
||||
placeholder="e.g., Positive adjectives for product descriptions"
|
||||
className="bg-slate-900 border-slate-700"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</CRUDModal>
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<DeleteConfirm
|
||||
isOpen={isDeleteOpen}
|
||||
onClose={() => {
|
||||
setIsDeleteOpen(false);
|
||||
setDeletingDict(null);
|
||||
}}
|
||||
onConfirm={handleDelete}
|
||||
itemName={deletingDict?.category}
|
||||
isDeleting={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
interface CRUDModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
description?: string;
|
||||
onSubmit: () => void;
|
||||
isSubmitting?: boolean;
|
||||
children: React.ReactNode;
|
||||
submitText?: string;
|
||||
cancelText?: string;
|
||||
}
|
||||
|
||||
export function CRUDModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
title,
|
||||
description,
|
||||
onSubmit,
|
||||
isSubmitting = false,
|
||||
children,
|
||||
submitText = 'Save',
|
||||
cancelText = 'Cancel',
|
||||
}: CRUDModalProps) {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="bg-slate-800 border-slate-700 text-white max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold text-white">{title}</DialogTitle>
|
||||
{description && (
|
||||
<DialogDescription className="text-slate-400">
|
||||
{description}
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
<div className="py-4">{children}</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={isSubmitting}
|
||||
className="bg-slate-700 border-slate-600 hover:bg-slate-600"
|
||||
>
|
||||
{cancelText}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onSubmit}
|
||||
disabled={isSubmitting}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{isSubmitting ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
Saving...
|
||||
</span>
|
||||
) : (
|
||||
submitText
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getSortedRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
flexRender,
|
||||
type ColumnDef,
|
||||
type SortingState,
|
||||
type ColumnFiltersState,
|
||||
} from '@tanstack/react-table';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
|
||||
interface DataTableProps<TData> {
|
||||
data: TData[];
|
||||
columns: ColumnDef<TData>[];
|
||||
onAdd?: () => void;
|
||||
onEdit?: (row: TData) => void;
|
||||
onDelete?: (row: TData) => void;
|
||||
onExport?: () => void;
|
||||
searchPlaceholder?: string;
|
||||
addButtonText?: string;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function DataTable<TData>({
|
||||
data,
|
||||
columns,
|
||||
onAdd,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onExport,
|
||||
searchPlaceholder = 'Search...',
|
||||
addButtonText = 'Add New',
|
||||
isLoading = false,
|
||||
}: DataTableProps<TData>) {
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
globalFilter,
|
||||
},
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: 20,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Toolbar */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex-1 max-w-sm">
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
value={globalFilter ?? ''}
|
||||
onChange={(e) => setGlobalFilter(e.target.value)}
|
||||
className="bg-slate-900 border-slate-700"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{onExport && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onExport}
|
||||
className="bg-slate-800 border-slate-700 hover:bg-slate-700"
|
||||
>
|
||||
📤 Export
|
||||
</Button>
|
||||
)}
|
||||
{onAdd && (
|
||||
<Button
|
||||
onClick={onAdd}
|
||||
className="bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
✨ {addButtonText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="rounded-lg border border-slate-700 bg-slate-800 overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-900">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<tr key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<th
|
||||
key={header.id}
|
||||
className="px-6 py-3 text-left text-xs font-medium text-slate-400 uppercase tracking-wider cursor-pointer hover:text-slate-200"
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{{
|
||||
asc: ' 🔼',
|
||||
desc: ' 🔽',
|
||||
}[header.column.getIsSorted() as string] ?? null}
|
||||
</div>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-700">
|
||||
{isLoading ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={columns.length}
|
||||
className="px-6 py-12 text-center text-slate-500"
|
||||
>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
|
||||
Loading...
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : table.getRowModel().rows.length === 0 ? (
|
||||
<tr>
|
||||
<td
|
||||
colSpan={columns.length}
|
||||
className="px-6 py-12 text-center text-slate-500"
|
||||
>
|
||||
No data found
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<tr
|
||||
key={row.id}
|
||||
className="hover:bg-slate-700/50 transition-colors"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<td key={cell.id} className="px-6 py-4 text-sm text-slate-300">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{table.getPageCount() > 1 && (
|
||||
<div className="flex items-center justify-between px-6 py-4 bg-slate-900 border-t border-slate-700">
|
||||
<div className="text-sm text-slate-400">
|
||||
Showing {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} to{' '}
|
||||
{Math.min(
|
||||
(table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize,
|
||||
table.getFilteredRowModel().rows.length
|
||||
)}{' '}
|
||||
of {table.getFilteredRowModel().rows.length} results
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
className="bg-slate-800 border-slate-700"
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
className="bg-slate-800 border-slate-700"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
|
||||
interface DeleteConfirmProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onConfirm: () => void;
|
||||
title?: string;
|
||||
description?: string;
|
||||
itemName?: string;
|
||||
isDeleting?: boolean;
|
||||
}
|
||||
|
||||
export function DeleteConfirm({
|
||||
isOpen,
|
||||
onClose,
|
||||
onConfirm,
|
||||
title = 'Are you sure?',
|
||||
description,
|
||||
itemName,
|
||||
isDeleting = false,
|
||||
}: DeleteConfirmProps) {
|
||||
const defaultDescription = itemName
|
||||
? `This will permanently delete "${itemName}". This action cannot be undone.`
|
||||
: 'This action cannot be undone. This will permanently delete the item.';
|
||||
|
||||
return (
|
||||
<AlertDialog open={isOpen} onOpenChange={onClose}>
|
||||
<AlertDialogContent className="bg-slate-800 border-slate-700">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle className="text-white">{title}</AlertDialogTitle>
|
||||
<AlertDialogDescription className="text-slate-400">
|
||||
{description || defaultDescription}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel
|
||||
disabled={isDeleting}
|
||||
className="bg-slate-700 border-slate-600 hover:bg-slate-600 text-white"
|
||||
>
|
||||
Cancel
|
||||
</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={onConfirm}
|
||||
disabled={isDeleting}
|
||||
className="bg-red-600 hover:bg-red-700 text-white"
|
||||
>
|
||||
{isDeleting ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
||||
Deleting...
|
||||
</span>
|
||||
) : (
|
||||
'Delete'
|
||||
)}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
import AdminLayout from '@/layouts/AdminLayout.astro';
|
||||
import AvatarVariantManager from '@/components/admin/collections/AvatarVariantManager';
|
||||
---
|
||||
|
||||
<AdminLayout title="Avatar Variants">
|
||||
<div class="p-8">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">🎭 Avatar Variants</h1>
|
||||
<p class="text-slate-400">Manage gender and tone variations for avatars</p>
|
||||
</div>
|
||||
|
||||
<AvatarVariantManager client:load />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
import AdminLayout from '@/layouts/AdminLayout.astro';
|
||||
import GeoIntelligenceManager from '@/components/admin/collections/GeoIntelligenceManager';
|
||||
---
|
||||
|
||||
<AdminLayout title="Geo Intelligence">
|
||||
<div class="p-8">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">🗺️ Geo Intelligence</h1>
|
||||
<p class="text-slate-400">Location targeting and geographic data</p>
|
||||
</div>
|
||||
|
||||
<GeoIntelligenceManager client:load />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
import AdminLayout from '@/layouts/AdminLayout.astro';
|
||||
import CartesianManagerEnhanced from '@/components/admin/content/CartesianManagerEnhanced';
|
||||
---
|
||||
|
||||
<AdminLayout title="Cartesian Patterns">
|
||||
<div class="p-8">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">🔀 Cartesian Patterns</h1>
|
||||
<p class="text-slate-400">Define content generation patterns using Cartesian product logic</p>
|
||||
</div>
|
||||
|
||||
<CartesianManagerEnhanced client:load />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
@@ -1,15 +0,0 @@
|
||||
---
|
||||
import AdminLayout from '@/layouts/AdminLayout.astro';
|
||||
import SpintaxManagerEnhanced from '@/components/admin/content/SpintaxManagerEnhanced';
|
||||
---
|
||||
|
||||
<AdminLayout title="Spintax Dictionaries">
|
||||
<div class="p-8">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-3xl font-bold text-white mb-2">📚 Spintax Dictionaries</h1>
|
||||
<p class="text-slate-400">Manage synonym variations for content generation</p>
|
||||
</div>
|
||||
|
||||
<SpintaxManagerEnhanced client:load />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
Reference in New Issue
Block a user