This repository has been archived on 2026-02-01. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
synthetic-consumer-data/components/Content.tsx
2024-12-29 20:30:55 +01:00

337 lines
13 KiB
TypeScript

import { useState } from 'react';
import { Consumer, consumerSchema } from '@utils/consumer/types';
import { PurchaseList, purchasesRequestSchema } from '@purchases/types';
import { Button } from './Button';
import { useToast } from '../context/toast/ToastContext';
import { Toasts } from './Toast';
import { LineChart, PersonStanding, Download, Sparkles } from 'lucide-react';
import axios from 'axios';
export const Content = () => {
const [consumerId, setConsumerId] = useState<number>();
const [loading, setLoading] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [consumer, setConsumer] = useState<Consumer | null>(null);
const [purchasesError, setPurchasesError] = useState<string | null>(null);
const [editedConsumer, setEditedConsumer] = useState('');
const [purchasesResult, setPurchasesResult] = useState<PurchaseList | null>(
null
);
const { showToast, toasts } = useToast();
const downloadJson = (data: Consumer | PurchaseList, filename: string) => {
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: 'application/json'
});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
};
const handleDownloadConsumer = () => {
if (!consumer) return;
try {
downloadJson(consumer, 'consumer.json');
} catch (err) {
showToast('Failed to download consumer data');
}
};
const handleDownloadPurchases = () => {
if (!purchasesResult) return;
try {
downloadJson(purchasesResult, 'purchases.json');
} catch (err) {
showToast('Failed to download purchase history');
}
};
const handleGenerateConsumer = async () => {
setLoading(true);
setConsumer(null);
setPurchasesError(null);
setEditedConsumer('');
setPurchasesResult(null);
try {
const { data } = await axios.post(
'/api/consumer',
{},
{
headers: { 'Content-Type': 'application/json' }
}
);
setConsumerId(data.id);
setConsumer(data.consumer);
setEditedConsumer(JSON.stringify(data.consumer, null, 2));
} catch (err) {
if (axios.isAxiosError(err)) {
const errorMessage = err.response?.data?.error || err.message;
showToast(errorMessage);
} else {
showToast('Something went wrong');
}
} finally {
setLoading(false);
}
};
const handleJsonEdit = (value: string) => {
setEditedConsumer(value);
try {
const parsed = JSON.parse(value);
const validConsumer = consumerSchema.safeParse(parsed);
if (!validConsumer.success) {
setPurchasesError('Invalid consumer format');
return;
}
setPurchasesError(null);
} catch {
setPurchasesError('Invalid JSON format');
}
};
const handleGeneratePurchases = async () => {
if (purchasesError) {
showToast('Please fix the JSON errors before submitting');
return;
}
setSubmitting(true);
setPurchasesResult(null);
try {
const jsonData = JSON.parse(editedConsumer);
const requestData = { id: consumerId, consumer: jsonData };
const validationResult = purchasesRequestSchema.safeParse(requestData);
if (!validationResult.success) {
throw new Error(validationResult.error.issues[0].message);
}
const { data } = await axios.post('/api/purchase-list', requestData, {
headers: { 'Content-Type': 'application/json' }
});
setPurchasesResult(data);
} catch (err) {
if (axios.isAxiosError(err)) {
const errorMessage = err.response?.data?.error || err.message;
showToast(errorMessage);
} else if (err instanceof Error) {
showToast(err.message);
} else {
showToast('Something went wrong');
}
} finally {
setSubmitting(false);
}
};
return (
<div className='min-h-screen relative'>
<div
className='fixed inset-0 bg-white'
style={{
backgroundImage: `
linear-gradient(30deg, #e2e8f0 12%, transparent 12.5%, transparent 87.5%, #e2e8f0 87.5%, #e2e8f0),
linear-gradient(150deg, #e2e8f0 12%, transparent 12.5%, transparent 87.5%, #e2e8f0 87.5%, #e2e8f0),
linear-gradient(30deg, #e2e8f0 12%, transparent 12.5%, transparent 87.5%, #e2e8f0 87.5%, #e2e8f0),
linear-gradient(150deg, #e2e8f0 12%, transparent 12.5%, transparent 87.5%, #e2e8f0 87.5%, #e2e8f0),
linear-gradient(60deg, rgba(226,232,240,0.25) 25%, transparent 25.5%, transparent 75%, rgba(226,232,240,0.25) 75%, rgba(226,232,240,0.25)),
linear-gradient(60deg, rgba(226,232,240,0.25) 25%, transparent 25.5%, transparent 75%, rgba(226,232,240,0.25) 75%, rgba(226,232,240,0.25))
`,
backgroundSize: '80px 140px',
backgroundPosition: '0 0, 0 0, 40px 70px, 40px 70px, 0 0, 40px 70px',
opacity: 0.4
}}
/>
<div className='container mx-auto px-4 py-12'>
<Toasts toasts={toasts} />
<div className='max-w-7xl mx-auto mb-12'>
<div className='bg-gradient-to-r from-white/70 to-white/90 backdrop-blur-sm rounded-2xl border border-slate-200 shadow-xl p-6 hover:shadow-2xl transition-all duration-300'>
<div className='flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6'>
<div className='flex items-center gap-3'>
<div className='p-2 bg-indigo-100 rounded-lg'>
<Sparkles className='w-5 h-5 text-indigo-600' />
</div>
<div>
<h2 className='text-lg font-semibold text-slate-900'>
Quick Start
</h2>
<p className='text-sm text-slate-600'>
Generate synthetic data in seconds
</p>
</div>
</div>
<div className='flex gap-2'>
<a
href='/samples/consumer.json'
download
className='flex-1 md:flex-none inline-flex items-center justify-center gap-2 px-3 py-2 bg-slate-100 hover:bg-slate-200
text-slate-700 rounded-lg text-sm font-medium transition-colors'
>
<Download className='w-4 h-4' />
Sample Consumer
</a>
<a
href='/samples/purchases.json'
download
className='flex-1 md:flex-none inline-flex items-center justify-center gap-2 px-3 py-2 bg-slate-100 hover:bg-slate-200
text-slate-700 rounded-lg text-sm font-medium transition-colors'
>
<Download className='w-4 h-4' />
Sample Purchases
</a>
</div>
</div>
<div className='grid md:grid-cols-3 gap-4'>
<div className='p-4 bg-blue-50/50 rounded-lg border border-blue-100'>
<div className='text-xl font-bold text-blue-600 mb-1'>01</div>
<h3 className='font-medium text-slate-900'>Generate Profile</h3>
<p className='text-sm text-slate-600'>
Create a synthetic consumer profile
</p>
</div>
<div className='p-4 bg-indigo-50/50 rounded-lg border border-indigo-100'>
<div className='text-xl font-bold text-indigo-600 mb-1'>02</div>
<h3 className='font-medium text-slate-900'>Review Data</h3>
<p className='text-sm text-slate-600'>
Check and modify the generated profile
</p>
</div>
<div className='p-4 bg-purple-50/50 rounded-lg border border-purple-100'>
<div className='text-xl font-bold text-purple-600 mb-1'>03</div>
<h3 className='font-medium text-slate-900'>Get History</h3>
<p className='text-sm text-slate-600'>
Generate purchase history data
</p>
</div>
</div>
</div>
</div>
<div className='grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-7xl mx-auto'>
<div className='bg-white/70 backdrop-blur-sm rounded-2xl border border-slate-200 shadow-xl hover:shadow-2xl transition-all duration-300'>
<div className='p-6 border-b border-slate-100 bg-gradient-to-r from-blue-50 to-indigo-50'>
<div className='flex items-center gap-4'>
<div className='p-2 bg-blue-100 rounded-lg'>
<PersonStanding className='w-5 h-5 text-blue-600' />
</div>
<h2 className='text-xl font-semibold text-slate-900'>
Consumer Data
</h2>
</div>
</div>
<div className='p-8 space-y-6'>
<Button
loading={loading}
disabled={loading || submitting}
labelLoading='Generating consumer... (it can take up to 30 seconds)'
labelReady='Generate Consumer Profile'
onClick={handleGenerateConsumer}
/>
<div className='space-y-3'>
<div className='flex justify-between items-center'>
<label className='text-sm font-medium text-slate-700'>
Edit Consumer Data
</label>
{purchasesError && (
<span className='text-sm text-red-600 bg-red-50 px-4 py-1 rounded-full'>
{purchasesError}
</span>
)}
</div>
<div className='relative'>
<textarea
value={editedConsumer}
onChange={e => handleJsonEdit(e.target.value)}
className='w-full h-96 p-4 font-mono text-sm
bg-slate-50 rounded-xl border border-slate-200
focus:ring-2 focus:ring-blue-500 focus:border-blue-500
resize-none transition-all duration-200'
spellCheck='false'
disabled={submitting}
placeholder={
consumer
? ''
: 'Generated consumer profile will appear here'
}
/>
</div>
</div>
<Button
labelReady='Download generated consumer data'
onClick={handleDownloadConsumer}
disabled={!consumer}
className='w-full inline-flex items-center justify-center gap-2 px-4 py-2 bg-slate-100 hover:bg-slate-200 disabled:opacity-50 disabled:cursor-not-allowed text-slate-700 rounded-lg text-sm font-medium transition-colors'
/>
<Button
loading={submitting}
disabled={
!editedConsumer || Boolean(purchasesError) || submitting
}
labelLoading='Generating purchases... (it can take up to a minute)'
labelReady='Generate Purchase History'
onClick={handleGeneratePurchases}
/>
</div>
</div>
<div className='bg-white/70 backdrop-blur-sm rounded-2xl border border-slate-200 shadow-xl hover:shadow-2xl transition-all duration-300'>
<div className='p-6 border-b border-slate-100 bg-gradient-to-r from-indigo-50 to-blue-50'>
<div className='flex items-center gap-4'>
<div className='p-2 bg-blue-100 rounded-lg'>
<LineChart className='w-5 h-5 text-blue-600' />
</div>
<h2 className='text-xl font-semibold text-slate-900'>
Purchase History
</h2>
</div>
</div>
<div className='p-8'>
<div className='h-[34rem] rounded-xl'>
{purchasesResult ? (
<div className='h-full bg-slate-50 border border-slate-200 rounded-xl'>
<pre className='text-sm text-slate-700 whitespace-pre-wrap p-6 h-full overflow-auto'>
{JSON.stringify(purchasesResult, null, 2)}
</pre>
</div>
) : (
<div
className='flex flex-col items-center justify-center h-full
bg-slate-50 border border-slate-200 rounded-xl gap-4'
>
<LineChart className='w-12 h-12 text-slate-300' />
<p className='text-sm font-medium text-slate-500'>
No purchase history generated yet
</p>
</div>
)}
</div>
</div>
<div className='px-8'>
<Button
labelReady='Download generated purchase history data'
onClick={handleDownloadPurchases}
disabled={!purchasesResult}
className='w-full inline-flex items-center justify-center gap-2 px-4 py-2 bg-slate-100 hover:bg-slate-200 disabled:opacity-50 disabled:cursor-not-allowed text-slate-700 rounded-lg text-sm font-medium transition-colors'
/>
</div>
</div>
</div>
</div>
</div>
);
};