import React, { useState, useEffect } from 'react';
import { createRoot } from 'react-dom/client';
import {
Plus,
Inbox,
Calendar as CalendarIcon,
Settings,
CheckCircle,
Circle,
Trash2,
Database,
AlertCircle,
Clock,
ChevronLeft,
ChevronRight,
ChevronDown,
ChevronUp,
X,
Tag,
Save,
Zap,
PauseCircle,
Archive,
List,
ArrowRightCircle,
Grid,
Folder,
FolderPlus,
CornerUpLeft,
Briefcase,
Layout,
PieChart,
BrainCircuit,
Check
} from 'lucide-react';
// --- Constantes ---
const CATEGORIES = [
"💻 Trabajo",
"🏠 Personal",
"💼 Gestión / Administración",
"📈 Negocio",
"🎯 Marketing",
"🤝 Clientes",
"💰 Finanzas",
"📞 Llamadas",
"✉️ Emails",
"🛠️ Mantenimiento / Soporte",
"📦 Compras / Pendientes",
"🎓 Formación"
];
// --- Componentes UI Básicos ---
const Button = ({ children, onClick, variant = 'primary', className = '', type = 'button' }) => {
const baseStyle = "px-4 py-3 rounded-xl font-medium transition-all duration-200 flex items-center justify-center gap-2 active:scale-95 text-sm";
const variants = {
primary: "bg-violet-600 text-white hover:bg-violet-700 shadow-lg shadow-violet-200",
secondary: "bg-white border border-[#e7e5e4] text-stone-600 hover:bg-[#faf9f6]",
action: "bg-indigo-600 text-white hover:bg-indigo-700 shadow-md shadow-indigo-200",
danger: "bg-red-50 text-red-600 hover:bg-red-100",
ghost: "bg-transparent text-stone-500 hover:text-stone-900",
icon: "p-2 rounded-full hover:bg-[#faf9f6] text-stone-600"
};
return (
);
};
const Input = ({ label, value, onChange, placeholder, type = "text", className = "" }) => (
{label && }
);
const TextArea = ({ label, value, onChange, placeholder }) => (
{label && }
);
const Badge = ({ children, color = 'gray' }) => {
const colors = {
gray: 'bg-[#f5f5f4] text-stone-600',
blue: 'bg-blue-100 text-blue-700',
green: 'bg-green-100 text-green-700',
orange: 'bg-orange-100 text-orange-700',
purple: 'bg-purple-100 text-purple-700',
indigo: 'bg-indigo-100 text-indigo-700',
amber: 'bg-amber-100 text-amber-800',
teal: 'bg-teal-100 text-teal-800',
pink: 'bg-pink-100 text-pink-700',
cyan: 'bg-cyan-100 text-cyan-800',
violet: 'bg-violet-100 text-violet-700',
};
return (
{children}
);
};
// --- Componente Principal ---
export default function App() {
// Estados Generales
const [activeTab, setActiveTab] = useState('inbox');
const [tasks, setTasks] = useState([]);
const [projects, setProjects] = useState([]);
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const [isProjectModalOpen, setIsProjectModalOpen] = useState(false);
// Estado para Navegación de Proyectos
const [projectPath, setProjectPath] = useState([]);
// Estado para Detalle/Edición
const [selectedTask, setSelectedTask] = useState(null);
// Estados del Calendario Grid (Mes)
const [currentDate, setCurrentDate] = useState(new Date());
const [selectedCalendarDate, setSelectedCalendarDate] = useState(new Date());
// Estado para colapsar días en la vista Agenda
const [collapsedDays, setCollapsedDays] = useState({});
// Estado para nueva tarea
const [newTaskTitle, setNewTaskTitle] = useState('');
const [newTaskDate, setNewTaskDate] = useState('');
const [newTaskTime, setNewTaskTime] = useState('');
const [newTaskDesc, setNewTaskDesc] = useState('');
const [newTaskTag, setNewTaskTag] = useState(CATEGORIES[0]);
const [newTaskProject, setNewTaskProject] = useState(''); // ID del proyecto seleccionado
// Estados para Creación Rápida de Proyecto (dentro de Modal Tarea)
const [isQuickProjectMode, setIsQuickProjectMode] = useState(false);
const [quickProjectTitle, setQuickProjectTitle] = useState('');
// Estado para nuevo proyecto (Modal Proyectos Completo)
const [newProjectTitle, setNewProjectTitle] = useState('');
const [newProjectIcon, setNewProjectIcon] = useState('📁');
// Configuración Notion
const [notionConfig, setNotionConfig] = useState({ apiKey: '', databaseId: '' });
const [toast, setToast] = useState(null);
// --- Efectos ---
useEffect(() => {
const savedTasks = localStorage.getItem('rentalme-tasks-gtd');
const savedProjects = localStorage.getItem('rentalme-projects');
const savedNotion = localStorage.getItem('rentalme-notion-config');
if (savedTasks) setTasks(JSON.parse(savedTasks));
if (savedProjects) setProjects(JSON.parse(savedProjects));
if (savedNotion) setNotionConfig(JSON.parse(savedNotion));
}, []);
useEffect(() => {
localStorage.setItem('rentalme-tasks-gtd', JSON.stringify(tasks));
}, [tasks]);
useEffect(() => {
localStorage.setItem('rentalme-projects', JSON.stringify(projects));
}, [projects]);
useEffect(() => {
localStorage.setItem('rentalme-notion-config', JSON.stringify(notionConfig));
}, [notionConfig]);
// --- Helpers de Fecha ---
const getDaysInMonth = (date) => new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
const getFirstDayOfMonth = (date) => {
let day = new Date(date.getFullYear(), date.getMonth(), 1).getDay();
return day === 0 ? 6 : day - 1;
};
const changeMonth = (increment) => {
const newDate = new Date(currentDate.setMonth(currentDate.getMonth() + increment));
setCurrentDate(new Date(newDate));
};
const isSameDay = (d1, d2) => {
return d1.getDate() === d2.getDate() &&
d1.getMonth() === d2.getMonth() &&
d1.getFullYear() === d2.getFullYear();
};
const tasksForDate = (date) => {
const dateStr = date.toISOString().split('T')[0];
return tasks.filter(t => t.date === dateStr && !t.completed);
};
const formatDateHeader = (dateStr) => {
const date = new Date(dateStr);
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
today.setHours(0,0,0,0);
tomorrow.setHours(0,0,0,0);
const checkDate = new Date(date);
checkDate.setHours(0,0,0,0);
if (checkDate.getTime() === today.getTime()) return "Hoy";
if (checkDate.getTime() === tomorrow.getTime()) return "Mañana";
return date.toLocaleDateString('es-ES', { weekday: 'long', day: 'numeric', month: 'long' });
};
const isExpandedByDefault = (dateStr) => {
const today = new Date();
today.setHours(0,0,0,0);
const [y, m, d] = dateStr.split('-').map(Number);
const target = new Date(y, m - 1, d);
const diffTime = target - today;
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
return diffDays >= 0 && diffDays <= 2;
};
const toggleDayCollapse = (dateStr) => {
setCollapsedDays(prev => {
const isDefaultOpen = isExpandedByDefault(dateStr);
const currentlyCollapsed = prev[dateStr] !== undefined ? prev[dateStr] : !isDefaultOpen;
return { ...prev, [dateStr]: !currentlyCollapsed };
});
};
// --- Helpers de Proyecto ---
const getSubprojects = (parentId) => {
return projects.filter(p => p.parentId === (parentId || null));
};
const getProjectProgress = (projectId) => {
const projectTasks = tasks.filter(t => t.projectId === projectId);
if (projectTasks.length === 0) return 0;
const completed = projectTasks.filter(t => t.completed).length;
return Math.round((completed / projectTasks.length) * 100);
};
const deleteProject = (projectId) => {
if (window.confirm("¿Borrar proyecto y todas sus tareas?")) {
setProjects(projects.filter(p => p.id !== projectId));
setTasks(tasks.filter(t => t.projectId !== projectId));
showToast('Proyecto eliminado', 'neutral');
}
};
const createQuickProject = () => {
if (!quickProjectTitle.trim()) return;
const newProj = {
id: Date.now(),
title: quickProjectTitle,
icon: '📁',
parentId: null, // Se crea en raíz para simplificar desde quick add
createdAt: new Date().toISOString()
};
setProjects([...projects, newProj]);
setNewTaskProject(newProj.id); // Auto seleccionar el nuevo proyecto
setQuickProjectTitle('');
setIsQuickProjectMode(false);
showToast('Proyecto creado y seleccionado');
};
// --- Acciones ---
const showToast = (message, type = 'success') => {
setToast({ message, type });
setTimeout(() => setToast(null), 3000);
};
const closeAddModal = () => {
setIsAddModalOpen(false);
setNewTaskTitle('');
setNewTaskDate('');
setNewTaskTime('');
setNewTaskDesc('');
setNewTaskTag(CATEGORIES[0]);
setNewTaskProject('');
setIsQuickProjectMode(false);
setQuickProjectTitle('');
};
const addTask = (e) => {
e.preventDefault();
if (!newTaskTitle.trim()) return;
const initialStatus = newTaskDate ? 'next' : 'inbox';
let targetProjectId = null;
if (newTaskProject) {
targetProjectId = parseInt(newTaskProject);
} else if (projectPath.length > 0) {
targetProjectId = projectPath[projectPath.length - 1];
}
const task = {
id: Date.now(),
title: newTaskTitle,
date: newTaskDate || null,
time: newTaskTime || null,
description: newTaskDesc || '',
tag: newTaskTag,
status: initialStatus,
completed: false,
syncedToNotion: false,
projectId: targetProjectId,
createdAt: new Date().toISOString()
};
setTasks([task, ...tasks]);
closeAddModal();
showToast(initialStatus === 'inbox' ? 'Capturado en Inbox' : 'Añadido al Calendario');
};
const addProject = (e) => {
e.preventDefault();
if (!newProjectTitle.trim()) return;
const parentId = projectPath.length > 0 ? projectPath[projectPath.length - 1] : null;
const project = {
id: Date.now(),
title: newProjectTitle,
icon: newProjectIcon,
parentId: parentId,
createdAt: new Date().toISOString()
};
setProjects([...projects, project]);
setNewProjectTitle('');
setIsProjectModalOpen(false);
showToast('Proyecto creado');
};
const toggleTask = (id) => {
setTasks(tasks.map(t => t.id === id ? { ...t, completed: !t.completed } : t));
};
const saveEditedTask = () => {
if (!selectedTask || !selectedTask.title.trim()) return;
setTasks(tasks.map(t => t.id === selectedTask.id ? selectedTask : t));
setSelectedTask(null);
showToast('Tarea actualizada');
};
const deleteTask = (id) => {
setTasks(tasks.filter(t => t.id !== id));
if (selectedTask && selectedTask.id === id) setSelectedTask(null);
showToast('Tarea eliminada', 'neutral');
};
const changeStatus = (taskId, newStatus) => {
setTasks(tasks.map(t => t.id === taskId ? { ...t, status: newStatus } : t));
if (selectedTask && selectedTask.id === taskId) {
setSelectedTask({ ...selectedTask, status: newStatus });
}
showToast('Tarea movida');
};
const syncToNotion = async () => {
if (!notionConfig.apiKey || !notionConfig.databaseId) {
showToast('Falta API Key', 'error');
return;
}
showToast('Sincronizando...', 'neutral');
setTimeout(() => {
setTasks(tasks.map(t => ({ ...t, syncedToNotion: true })));
showToast('¡Sincronizado!', 'success');
}, 1500);
};
const getTagColor = (tag) => {
if (!tag) return 'gray';
if (tag.includes('Trabajo')) return 'violet';
if (tag.includes('Personal')) return 'green';
if (tag.includes('Gestión')) return 'purple';
if (tag.includes('Negocio')) return 'indigo';
if (tag.includes('Marketing')) return 'pink';
if (tag.includes('Clientes')) return 'teal';
if (tag.includes('Finanzas')) return 'amber';
if (tag.includes('Llamadas')) return 'cyan';
if (tag.includes('Emails')) return 'gray';
if (tag.includes('Mantenimiento')) return 'orange';
if (tag.includes('Compras')) return 'amber';
if (tag.includes('Formación')) return 'indigo';
return 'gray';
};
// --- FILTRADO Y AGRUPACIÓN ---
const inboxTasks = tasks.filter(t => !t.completed && t.status === 'inbox');
const somedayTasks = tasks.filter(t => !t.completed && t.status === 'someday'); // Tareas a futuro
const getGroupedAgendaTasks = () => {
// Agenda muestra tareas con fecha (independientemente del estado GTD antiguo)
const agendaTasks = tasks.filter(t =>
!t.completed &&
t.date
);
agendaTasks.sort((a, b) => {
if (a.date !== b.date) return a.date.localeCompare(b.date);
if (!a.time) return 1;
if (!b.time) return -1;
return a.time.localeCompare(b.time);
});
const groups = {};
agendaTasks.forEach(task => {
if (!groups[task.date]) groups[task.date] = [];
groups[task.date].push(task);
});
return groups;
};
const agendaGroups = getGroupedAgendaTasks();
const sortedDates = Object.keys(agendaGroups).sort();
// --- Componente TaskCard ---
const TaskCard = ({ task, onClick, showTime = true }) => {
const taskProject = task.projectId ? projects.find(p => p.id === parseInt(task.projectId)) : null;
return (
{task.title}
{task.description && (
{task.description}
)}
{taskProject && (
{taskProject.title}
)}
{showTime && task.time && (
{task.time}
)}
{!showTime && task.date && (
{new Date(task.date).toLocaleDateString('es-ES', {day: 'numeric', month: 'short'})}
)}
{task.tag}
);
};
return (
{/* Header Compacto con Rebranding, Ajustes y Futuro */}
{/* Contenido Principal */}
{/* --- INBOX --- */}
{activeTab === 'inbox' && (
Bandeja de Entrada
Tareas sin fecha o pendientes de procesar.
{inboxTasks.length === 0 ? (
Bandeja vacía.
Todas las tareas están organizadas.
) : (
Pendientes ({inboxTasks.length})
{inboxTasks.map(task => (
setSelectedTask(task)} showTime={false} />
))}
)}
)}
{/* --- FUTURO --- */}
{activeTab === 'future' && (
Futuro / Someday
Ideas y tareas para más adelante.
{somedayTasks.length === 0 ? (
No hay tareas archivadas.
) : (
Guardadas ({somedayTasks.length})
{somedayTasks.map(task => (
setSelectedTask(task)} showTime={false} />
))}
)}
)}
{/* --- AGENDA --- */}
{activeTab === 'agenda' && (
Mi Calendario
{sortedDates.length === 0 ? (
No hay tareas programadas próximamente.
) : (
{sortedDates.map(dateStr => {
const tasksForDay = agendaGroups[dateStr];
const headerText = formatDateHeader(dateStr);
const isToday = headerText === 'Hoy';
const isDefaultOpen = isExpandedByDefault(dateStr);
const isCollapsed = collapsedDays[dateStr] !== undefined ? collapsedDays[dateStr] : !isDefaultOpen;
return (
{!isCollapsed && (
{tasksForDay.map(task => (
setSelectedTask(task)} showTime={true} />
))}
)}
{isCollapsed &&
}
);
})}
)}
)}
{/* --- PROYECTOS --- */}
{activeTab === 'projects' && (
Proyectos
{projectPath.length > 0 && (
)}
{/* Breadcrumb */}
setProjectPath([])} className={`cursor-pointer ${projectPath.length === 0 ? 'font-bold text-stone-700' : 'hover:text-violet-600'}`}>Inicio
{projectPath.map((pid, idx) => {
const p = projects.find(proj => proj.id === pid);
if (!p) return null;
const isLast = idx === projectPath.length - 1;
return (
setProjectPath(projectPath.slice(0, idx + 1))}
className={`cursor-pointer ${isLast ? 'font-bold text-stone-700' : 'hover:text-violet-600'}`}
>
{p.title}
);
})}
{projectPath.length === 0 ? 'Mis Proyectos' : 'Subproyectos'}
{getSubprojects(projectPath.length > 0 ? projectPath[projectPath.length - 1] : null).length === 0 ? (
No hay {projectPath.length === 0 ? 'proyectos' : 'subproyectos'}.
) : (
{getSubprojects(projectPath.length > 0 ? projectPath[projectPath.length - 1] : null).map(proj => {
const progress = getProjectProgress(proj.id);
return (
setProjectPath([...projectPath, proj.id])}
className="bg-white p-3 rounded-xl shadow-sm border border-stone-100 hover:shadow-md transition-all cursor-pointer relative group"
>
{proj.icon}
{proj.title}
{progress}%
);
})}
)}
{projectPath.length > 0 && (
Tareas del Proyecto
{tasks.filter(t => t.projectId === projectPath[projectPath.length - 1]).length === 0 ? (
Este proyecto no tiene tareas asignadas.
) : (
tasks.filter(t => t.projectId === projectPath[projectPath.length - 1]).map(task => (
setSelectedTask(task)} showTime={!!task.time} />
))
)}
)}
)}
{/* --- MES --- */}
{activeTab === 'month' && (
{currentDate.toLocaleDateString('es-ES', { month: 'long', year: 'numeric' })}
{['L', 'M', 'X', 'J', 'V', 'S', 'D'].map(day => (
{day}
))}
{(() => {
const days = [];
const startDay = getFirstDayOfMonth(currentDate);
for (let i = 0; i < startDay; i++) days.push(
);
for (let i = 1; i <= getDaysInMonth(currentDate); i++) {
const d = new Date(currentDate.getFullYear(), currentDate.getMonth(), i);
const t = tasksForDate(d);
const isSel = isSameDay(d, selectedCalendarDate);
days.push(
);
}
return days;
})()}
{selectedCalendarDate.toLocaleDateString('es-ES', { weekday: 'long', day: 'numeric' })}
{tasksForDate(selectedCalendarDate).map(task => (
setSelectedTask(task)} showTime={true} />
))}
)}
{/* --- AJUSTES --- */}
{activeTab === 'settings' && (
)}
{/* --- FAB (Capturar) --- */}
{/* --- Bottom Nav --- */}
{/* --- MODAL EDICIÓN --- */}
{selectedTask && (
setSelectedTask(null)}>
e.stopPropagation()}>
{selectedTask.tag}
setSelectedTask({...selectedTask, title: e.target.value})}
className="w-full text-xl font-bold text-stone-800 bg-transparent border-none focus:ring-0 p-0 mb-4 placeholder-stone-300"
placeholder="Nombre de la tarea"
/>
{/* Selector de Proyecto en Edición */}
Mover a
)}
{/* --- MODAL NUEVO PROYECTO --- */}
{isProjectModalOpen && (
{projectPath.length === 0 ? 'Nuevo Proyecto' : 'Nuevo Subproyecto'}
)}
{/* --- MODAL NUEVA TAREA (Captura Rápida) --- */}
{isAddModalOpen && (
)}
{/* Toast Notification */}
{toast && (
{toast.type === 'success' &&
}
{toast.type === 'error' &&
}
{toast.message}
)}
);
}
// --- Montaje de la Aplicación (Crucial para Hostinger) ---
const container = document.getElementById('root');
const root = createRoot(container);
root.render();