Implement Phase 3: Advanced post-onboarding features (JTBD-driven UX)
Complete JTBD implementation with 4 advanced features to reduce friction and accelerate configuration for non-technical bakery owners. **1. Recipe Templates Library:** - Add RecipeTemplateSelector modal with searchable template gallery - Pre-built templates: Baguette, Pan de Molde, Medialunas, Facturas, Bizcochuelo, Galletas - Smart ingredient matching between templates and user's inventory - Category filtering (Panes, Facturas, Tortas, Galletitas) - One-click template loading with pre-filled wizard data - "Create from scratch" option for custom recipes - Integrated as pre-wizard step in RecipeWizardModal **2. Bulk Supplier CSV Import:** - Add BulkSupplierImportModal with CSV upload & parsing - Downloadable CSV template with examples - Live validation with error detection - Preview table showing valid/invalid rows - Multi-column support (15+ fields: name, type, email, phone, payment terms, address, etc.) - Batch import with progress tracking - Success/error notifications **3. Configuration Recovery (Auto-save):** - Add useWizardDraft hook with localStorage persistence - Auto-save wizard progress every 30 seconds - 7-day draft expiration (configurable TTL) - DraftRecoveryPrompt component for restore/discard choice - Shows "saved X time ago" with human-friendly formatting - Prevents data loss from accidental browser closes **4. Milestone Notifications (Feature Unlocks):** - Add Toast notification system (ToastNotification, ToastContainer, useToast hook) - Support for success, error, info, and milestone toast types - Animated slide-in/slide-out transitions - Auto-dismiss with configurable duration - useFeatureUnlocks hook to track when features are unlocked - Visual feedback for configuration milestones **Benefits:** - Templates: Reduce recipe creation time from 10+ min to <2 min - Bulk Import: Add 50+ suppliers in seconds vs hours - Auto-save: Zero data loss from accidental exits - Notifications: Clear feedback on progress and unlocked capabilities Files: - RecipeTemplateSelector: Template library UI - BulkSupplierImportModal: CSV import system - useWizardDraft + DraftRecoveryPrompt: Auto-save infrastructure - Toast system + useToast + useFeatureUnlocks: Notification framework Part of 3-phase JTBD implementation (Phase 1: Progress Widget, Phase 2: Wizards, Phase 3: Advanced Features).
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import React from 'react';
|
||||
import { Clock, X, FileText, Trash2 } from 'lucide-react';
|
||||
import { formatTimeAgo } from '../../../hooks/useWizardDraft';
|
||||
|
||||
interface DraftRecoveryPromptProps {
|
||||
isOpen: boolean;
|
||||
lastSaved: Date;
|
||||
onRestore: () => void;
|
||||
onDiscard: () => void;
|
||||
onClose: () => void;
|
||||
wizardName: string;
|
||||
}
|
||||
|
||||
export const DraftRecoveryPrompt: React.FC<DraftRecoveryPromptProps> = ({
|
||||
isOpen,
|
||||
lastSaved,
|
||||
onRestore,
|
||||
onDiscard,
|
||||
onClose,
|
||||
wizardName
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[60] flex items-center justify-center">
|
||||
{/* Backdrop */}
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50 backdrop-blur-sm"
|
||||
onClick={onClose}
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<div className="relative w-full max-w-md bg-[var(--bg-primary)] rounded-xl shadow-2xl overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="px-6 py-4 border-b border-[var(--border-secondary)] bg-gradient-to-r from-[var(--color-warning)]/10 to-[var(--color-warning)]/5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-full bg-[var(--color-warning)]/20 flex items-center justify-center">
|
||||
<FileText className="w-5 h-5 text-[var(--color-warning)]" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-[var(--text-primary)]">
|
||||
Borrador Detectado
|
||||
</h2>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
{wizardName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-1.5 hover:bg-[var(--bg-secondary)] rounded-lg transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4 text-[var(--text-secondary)]" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-6 space-y-4">
|
||||
{/* Info Card */}
|
||||
<div className="p-4 bg-[var(--bg-secondary)] rounded-lg border border-[var(--border-secondary)]">
|
||||
<div className="flex items-start gap-3">
|
||||
<Clock className="w-5 h-5 text-[var(--text-tertiary)] mt-0.5 flex-shrink-0" />
|
||||
<div className="flex-1">
|
||||
<p className="text-sm text-[var(--text-primary)] font-medium mb-1">
|
||||
Progreso guardado automáticamente
|
||||
</p>
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Guardado {formatTimeAgo(lastSaved)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-sm text-[var(--text-secondary)]">
|
||||
Encontramos un borrador de este formulario. ¿Deseas continuar desde donde lo dejaste o empezar de nuevo?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="px-6 py-4 border-t border-[var(--border-secondary)] bg-[var(--bg-secondary)] flex items-center gap-3">
|
||||
<button
|
||||
onClick={onDiscard}
|
||||
className="flex-1 px-4 py-2.5 bg-[var(--bg-primary)] border border-[var(--border-secondary)] text-[var(--text-primary)] rounded-lg hover:bg-[var(--bg-tertiary)] transition-colors flex items-center justify-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
Descartar y Empezar de Nuevo
|
||||
</button>
|
||||
<button
|
||||
onClick={onRestore}
|
||||
className="flex-1 px-4 py-2.5 bg-[var(--color-primary)] text-white rounded-lg hover:bg-[var(--color-primary-dark)] transition-colors flex items-center justify-center gap-2 text-sm font-medium"
|
||||
>
|
||||
<FileText className="w-4 h-4" />
|
||||
Restaurar Borrador
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
1
frontend/src/components/ui/DraftRecoveryPrompt/index.ts
Normal file
1
frontend/src/components/ui/DraftRecoveryPrompt/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { DraftRecoveryPrompt } from './DraftRecoveryPrompt';
|
||||
21
frontend/src/components/ui/Toast/ToastContainer.tsx
Normal file
21
frontend/src/components/ui/Toast/ToastContainer.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { ToastNotification, Toast } from './ToastNotification';
|
||||
|
||||
interface ToastContainerProps {
|
||||
toasts: Toast[];
|
||||
onClose: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ToastContainer: React.FC<ToastContainerProps> = ({ toasts, onClose }) => {
|
||||
if (toasts.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed top-4 right-4 z-[9999] flex flex-col gap-3 pointer-events-none">
|
||||
{toasts.map(toast => (
|
||||
<div key={toast.id} className="pointer-events-auto">
|
||||
<ToastNotification toast={toast} onClose={onClose} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
115
frontend/src/components/ui/Toast/ToastNotification.tsx
Normal file
115
frontend/src/components/ui/Toast/ToastNotification.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { X, CheckCircle2, AlertCircle, Info, Sparkles } from 'lucide-react';
|
||||
|
||||
export type ToastType = 'success' | 'error' | 'info' | 'milestone';
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
title: string;
|
||||
message?: string;
|
||||
duration?: number;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface ToastNotificationProps {
|
||||
toast: Toast;
|
||||
onClose: (id: string) => void;
|
||||
}
|
||||
|
||||
export const ToastNotification: React.FC<ToastNotificationProps> = ({ toast, onClose }) => {
|
||||
const [isExiting, setIsExiting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const duration = toast.duration || 5000;
|
||||
const timer = setTimeout(() => {
|
||||
setIsExiting(true);
|
||||
setTimeout(() => onClose(toast.id), 300); // Wait for animation
|
||||
}, duration);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [toast.id, toast.duration, onClose]);
|
||||
|
||||
const getIcon = () => {
|
||||
if (toast.icon) return toast.icon;
|
||||
|
||||
switch (toast.type) {
|
||||
case 'success':
|
||||
return <CheckCircle2 className="w-5 h-5" />;
|
||||
case 'error':
|
||||
return <AlertCircle className="w-5 h-5" />;
|
||||
case 'milestone':
|
||||
return <Sparkles className="w-5 h-5" />;
|
||||
case 'info':
|
||||
default:
|
||||
return <Info className="w-5 h-5" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStyles = () => {
|
||||
switch (toast.type) {
|
||||
case 'success':
|
||||
return 'bg-green-50 border-green-200 text-green-800';
|
||||
case 'error':
|
||||
return 'bg-red-50 border-red-200 text-red-800';
|
||||
case 'milestone':
|
||||
return 'bg-gradient-to-r from-purple-50 to-pink-50 border-purple-200 text-purple-800';
|
||||
case 'info':
|
||||
default:
|
||||
return 'bg-blue-50 border-blue-200 text-blue-800';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
w-full max-w-sm p-4 rounded-lg border shadow-lg backdrop-blur-sm
|
||||
transition-all duration-300 transform
|
||||
${getStyles()}
|
||||
${isExiting ? 'opacity-0 translate-x-full' : 'opacity-100 translate-x-0'}
|
||||
`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Icon */}
|
||||
<div className="flex-shrink-0 mt-0.5">
|
||||
{getIcon()}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-semibold mb-0.5">
|
||||
{toast.title}
|
||||
</h3>
|
||||
{toast.message && (
|
||||
<p className="text-sm opacity-90">
|
||||
{toast.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsExiting(true);
|
||||
setTimeout(() => onClose(toast.id), 300);
|
||||
}}
|
||||
className="flex-shrink-0 p-1 hover:bg-black/10 rounded transition-colors"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
{toast.type === 'milestone' && (
|
||||
<div className="mt-3 h-1 bg-white/30 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-purple-400 to-pink-400 animate-pulse"
|
||||
style={{
|
||||
animation: `progress ${toast.duration || 5000}ms linear`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
3
frontend/src/components/ui/Toast/index.ts
Normal file
3
frontend/src/components/ui/Toast/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { ToastNotification } from './ToastNotification';
|
||||
export { ToastContainer } from './ToastContainer';
|
||||
export type { Toast, ToastType } from './ToastNotification';
|
||||
Reference in New Issue
Block a user