From 5a4423edaf5fe36d17cceedbcf8b416c52879485 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 7 May 2026 00:33:03 +0000 Subject: [PATCH] Add browser-based task manager app with email summaries Single-page HTML/CSS/JS app that lets users create, filter and delete tasks (persisted in localStorage) and send a single task or a filtered summary via mailto:, so it runs fully in the browser with no backend. --- task-manager-app/README.md | 29 ++++ task-manager-app/app.js | 277 ++++++++++++++++++++++++++++++++ task-manager-app/index.html | 97 +++++++++++ task-manager-app/styles.css | 310 ++++++++++++++++++++++++++++++++++++ 4 files changed, 713 insertions(+) create mode 100644 task-manager-app/README.md create mode 100644 task-manager-app/app.js create mode 100644 task-manager-app/index.html create mode 100644 task-manager-app/styles.css diff --git a/task-manager-app/README.md b/task-manager-app/README.md new file mode 100644 index 0000000000..5a19f6327f --- /dev/null +++ b/task-manager-app/README.md @@ -0,0 +1,29 @@ +# Gestor de Tareas con Correo + +App de una sola página (HTML/CSS/JS) que corre completamente en el navegador. + +## Funcionalidades + +- Crear tareas con título, descripción, prioridad y fecha límite. +- Marcar como completadas o eliminarlas. +- Filtrar (todas / pendientes / completadas) y borrar las completadas en lote. +- Persistencia local con `localStorage` (no necesita servidor). +- Enviar una tarea individual o un resumen filtrado por correo. Se abre el cliente + de correo del usuario con un mensaje pre-llenado vía `mailto:`. + +## Cómo correrla + +Abrir `index.html` directamente en el navegador, o servirla localmente: + +```bash +cd task-manager-app +python3 -m http.server 8000 +# Luego abre http://localhost:8000 +``` + +## Notas + +- El envío usa `mailto:`, así que se necesita un cliente de correo configurado + (Gmail web, Outlook, Apple Mail, Thunderbird, etc.) para que el botón abra el + borrador. Esto evita necesitar un backend o claves de API. +- Las tareas viven en `localStorage` del navegador donde se creen. diff --git a/task-manager-app/app.js b/task-manager-app/app.js new file mode 100644 index 0000000000..77909d5784 --- /dev/null +++ b/task-manager-app/app.js @@ -0,0 +1,277 @@ +(() => { + "use strict"; + + const STORAGE_KEY = "task-manager-app:tasks:v1"; + + const PRIORITY_LABEL = { + baja: "Baja", + media: "Media", + alta: "Alta", + }; + + const els = { + taskForm: document.getElementById("task-form"), + title: document.getElementById("title"), + description: document.getElementById("description"), + priority: document.getElementById("priority"), + dueDate: document.getElementById("dueDate"), + list: document.getElementById("task-list"), + emptyState: document.getElementById("empty-state"), + filter: document.getElementById("filter"), + clearDone: document.getElementById("clear-done"), + emailForm: document.getElementById("email-form"), + recipient: document.getElementById("recipient"), + emailScope: document.getElementById("emailScope"), + }; + + let tasks = loadTasks(); + + function loadTasks() { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return []; + const parsed = JSON.parse(raw); + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } + } + + function saveTasks() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks)); + } + + function makeId() { + return Date.now().toString(36) + Math.random().toString(36).slice(2, 8); + } + + function addTask({ title, description, priority, dueDate }) { + tasks.unshift({ + id: makeId(), + title: title.trim(), + description: description.trim(), + priority, + dueDate: dueDate || "", + done: false, + createdAt: new Date().toISOString(), + }); + saveTasks(); + render(); + } + + function toggleTask(id) { + const t = tasks.find((x) => x.id === id); + if (!t) return; + t.done = !t.done; + saveTasks(); + render(); + } + + function deleteTask(id) { + tasks = tasks.filter((x) => x.id !== id); + saveTasks(); + render(); + } + + function clearCompleted() { + const remaining = tasks.filter((x) => !x.done); + if (remaining.length === tasks.length) return; + if (!confirm("¿Borrar todas las tareas completadas?")) return; + tasks = remaining; + saveTasks(); + render(); + } + + function filterTasks(scope) { + if (scope === "pending") return tasks.filter((t) => !t.done); + if (scope === "done") return tasks.filter((t) => t.done); + return tasks.slice(); + } + + function formatDate(iso) { + if (!iso) return ""; + const d = new Date(iso + "T00:00:00"); + if (Number.isNaN(d.getTime())) return iso; + return d.toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + }); + } + + function render() { + const scope = els.filter.value; + const visible = filterTasks(scope); + + els.list.replaceChildren(); + + if (visible.length === 0) { + els.emptyState.classList.remove("hidden"); + els.emptyState.textContent = + tasks.length === 0 + ? "No hay tareas todavía. ¡Agrega la primera!" + : "No hay tareas en este filtro."; + return; + } + + els.emptyState.classList.add("hidden"); + + for (const task of visible) { + els.list.appendChild(renderTask(task)); + } + } + + function renderTask(task) { + const li = document.createElement("li"); + li.className = "task-item" + (task.done ? " done" : ""); + li.dataset.id = task.id; + + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.className = "task-checkbox"; + checkbox.checked = task.done; + checkbox.setAttribute("aria-label", "Marcar como completada"); + checkbox.addEventListener("change", () => toggleTask(task.id)); + + const body = document.createElement("div"); + body.className = "task-body"; + + const title = document.createElement("p"); + title.className = "task-title"; + title.textContent = task.title; + body.appendChild(title); + + if (task.description) { + const desc = document.createElement("p"); + desc.className = "task-description"; + desc.textContent = task.description; + body.appendChild(desc); + } + + const meta = document.createElement("div"); + meta.className = "task-meta"; + + const priorityTag = document.createElement("span"); + priorityTag.className = "tag priority-" + task.priority; + priorityTag.textContent = "Prioridad: " + PRIORITY_LABEL[task.priority]; + meta.appendChild(priorityTag); + + if (task.dueDate) { + const dateTag = document.createElement("span"); + dateTag.className = "tag"; + dateTag.textContent = "Vence: " + formatDate(task.dueDate); + meta.appendChild(dateTag); + } + + body.appendChild(meta); + + const actions = document.createElement("div"); + actions.className = "task-actions"; + + const mailBtn = document.createElement("button"); + mailBtn.type = "button"; + mailBtn.className = "btn btn-icon"; + mailBtn.title = "Enviar esta tarea por correo"; + mailBtn.setAttribute("aria-label", "Enviar tarea por correo"); + mailBtn.textContent = "Correo"; + mailBtn.addEventListener("click", () => sendSingleTask(task)); + + const delBtn = document.createElement("button"); + delBtn.type = "button"; + delBtn.className = "btn btn-icon danger"; + delBtn.title = "Eliminar tarea"; + delBtn.setAttribute("aria-label", "Eliminar tarea"); + delBtn.textContent = "Eliminar"; + delBtn.addEventListener("click", () => { + if (confirm("¿Eliminar esta tarea?")) deleteTask(task.id); + }); + + actions.append(mailBtn, delBtn); + + li.append(checkbox, body, actions); + return li; + } + + function buildSummaryBody(taskList) { + if (taskList.length === 0) return "No hay tareas para mostrar."; + const lines = taskList.map((t, i) => { + const status = t.done ? "[X]" : "[ ]"; + const due = t.dueDate ? " (vence " + formatDate(t.dueDate) + ")" : ""; + const prio = " - prioridad " + PRIORITY_LABEL[t.priority]; + const desc = t.description ? "\n " + t.description.replace(/\n/g, "\n ") : ""; + return `${i + 1}. ${status} ${t.title}${prio}${due}${desc}`; + }); + return ( + "Resumen de tareas\n" + + "==================\n\n" + + lines.join("\n\n") + + "\n\n— Generado por Gestor de Tareas" + ); + } + + function openMailto({ to, subject, body }) { + const url = + "mailto:" + + encodeURIComponent(to) + + "?subject=" + + encodeURIComponent(subject) + + "&body=" + + encodeURIComponent(body); + window.location.href = url; + } + + function sendSummary({ to, scope }) { + const list = filterTasks(scope); + if (list.length === 0) { + alert("No hay tareas para enviar con este filtro."); + return; + } + const subject = + scope === "done" + ? "Tareas completadas" + : scope === "all" + ? "Resumen de todas las tareas" + : "Tareas pendientes"; + openMailto({ to, subject, body: buildSummaryBody(list) }); + } + + function sendSingleTask(task) { + const to = prompt("Correo del destinatario:"); + if (!to) return; + const trimmed = to.trim(); + if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed)) { + alert("Correo inválido."); + return; + } + const body = buildSummaryBody([task]); + openMailto({ to: trimmed, subject: "Tarea: " + task.title, body }); + } + + els.taskForm.addEventListener("submit", (e) => { + e.preventDefault(); + const title = els.title.value.trim(); + if (!title) return; + addTask({ + title, + description: els.description.value, + priority: els.priority.value, + dueDate: els.dueDate.value, + }); + els.taskForm.reset(); + els.priority.value = "media"; + els.title.focus(); + }); + + els.filter.addEventListener("change", render); + els.clearDone.addEventListener("click", clearCompleted); + + els.emailForm.addEventListener("submit", (e) => { + e.preventDefault(); + sendSummary({ + to: els.recipient.value.trim(), + scope: els.emailScope.value, + }); + }); + + render(); +})(); diff --git a/task-manager-app/index.html b/task-manager-app/index.html new file mode 100644 index 0000000000..f89a73c19c --- /dev/null +++ b/task-manager-app/index.html @@ -0,0 +1,97 @@ + + + + + + Gestor de Tareas con Correo + + + +
+

Gestor de Tareas

+

Organiza tus pendientes y envíalos por correo

+
+ +
+
+

Nueva tarea

+
+
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ + +
+
+ +
+
+

Mis tareas

+
+ + +
+
+ +
    +

    No hay tareas todavía. ¡Agrega la primera!

    +
    + +
    +

    Enviar resumen por correo

    +

    + Se abrirá tu cliente de correo con un mensaje pre-llenado que contiene tus tareas. +

    +
    +
    + + +
    + +
    + + +
    + + +
    +
    +
    + + + + + + diff --git a/task-manager-app/styles.css b/task-manager-app/styles.css new file mode 100644 index 0000000000..051a0d4e41 --- /dev/null +++ b/task-manager-app/styles.css @@ -0,0 +1,310 @@ +:root { + --bg: #f4f6fb; + --card: #ffffff; + --text: #1f2937; + --muted: #6b7280; + --primary: #2563eb; + --primary-hover: #1d4ed8; + --secondary: #e5e7eb; + --secondary-hover: #d1d5db; + --danger: #dc2626; + --success: #16a34a; + --border: #e5e7eb; + --priority-low: #16a34a; + --priority-med: #d97706; + --priority-high: #dc2626; + --shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, + Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; + background: var(--bg); + color: var(--text); + line-height: 1.5; +} + +.app-header { + text-align: center; + padding: 2rem 1rem 1rem; +} + +.app-header h1 { + margin: 0; + font-size: 2rem; + color: var(--primary); +} + +.subtitle { + margin: 0.25rem 0 0; + color: var(--muted); +} + +.container { + max-width: 760px; + margin: 0 auto; + padding: 1rem; + display: grid; + gap: 1.25rem; +} + +.card { + background: var(--card); + border-radius: 12px; + padding: 1.5rem; + box-shadow: var(--shadow); + border: 1px solid var(--border); +} + +.card h2 { + margin: 0 0 1rem; + font-size: 1.15rem; +} + +.task-form { + display: grid; + gap: 1rem; +} + +.form-row { + display: grid; + gap: 0.35rem; +} + +.form-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} + +label { + font-size: 0.875rem; + font-weight: 600; + color: var(--muted); +} + +input[type="text"], +input[type="email"], +input[type="date"], +textarea, +select { + width: 100%; + padding: 0.6rem 0.75rem; + border: 1px solid var(--border); + border-radius: 8px; + font: inherit; + background: #fff; + color: var(--text); + transition: border-color 0.15s, box-shadow 0.15s; +} + +input:focus, +textarea:focus, +select:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); +} + +textarea { + resize: vertical; +} + +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + padding: 0.6rem 1rem; + border: none; + border-radius: 8px; + font: inherit; + font-weight: 600; + cursor: pointer; + transition: background-color 0.15s, transform 0.05s; +} + +.btn:active { + transform: translateY(1px); +} + +.btn-primary { + background: var(--primary); + color: #fff; +} + +.btn-primary:hover { + background: var(--primary-hover); +} + +.btn-secondary { + background: var(--secondary); + color: var(--text); +} + +.btn-secondary:hover { + background: var(--secondary-hover); +} + +.btn-icon { + background: transparent; + color: var(--muted); + padding: 0.35rem 0.5rem; + border-radius: 6px; +} + +.btn-icon:hover { + background: var(--secondary); + color: var(--text); +} + +.btn-icon.danger:hover { + background: #fee2e2; + color: var(--danger); +} + +.toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-bottom: 1rem; + flex-wrap: wrap; +} + +.toolbar h2 { + margin: 0; +} + +.toolbar-actions { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.task-list { + list-style: none; + margin: 0; + padding: 0; + display: grid; + gap: 0.6rem; +} + +.task-item { + display: grid; + grid-template-columns: auto 1fr auto; + gap: 0.75rem; + align-items: start; + padding: 0.85rem 1rem; + background: #fafbfc; + border: 1px solid var(--border); + border-radius: 10px; +} + +.task-item.done .task-title { + text-decoration: line-through; + color: var(--muted); +} + +.task-checkbox { + margin-top: 0.25rem; + width: 1.1rem; + height: 1.1rem; + cursor: pointer; +} + +.task-body { + min-width: 0; +} + +.task-title { + margin: 0; + font-weight: 600; + word-wrap: break-word; +} + +.task-description { + margin: 0.25rem 0 0; + color: var(--muted); + font-size: 0.9rem; + white-space: pre-wrap; + word-wrap: break-word; +} + +.task-meta { + display: flex; + gap: 0.5rem; + margin-top: 0.5rem; + flex-wrap: wrap; + font-size: 0.78rem; +} + +.tag { + padding: 0.15rem 0.55rem; + border-radius: 999px; + background: var(--secondary); + color: var(--text); +} + +.tag.priority-baja { + background: #dcfce7; + color: var(--priority-low); +} + +.tag.priority-media { + background: #fef3c7; + color: var(--priority-med); +} + +.tag.priority-alta { + background: #fee2e2; + color: var(--priority-high); +} + +.task-actions { + display: flex; + gap: 0.25rem; +} + +.empty-state { + text-align: center; + color: var(--muted); + margin: 0; + padding: 1rem 0 0; +} + +.empty-state.hidden { + display: none; +} + +.hint { + margin: -0.25rem 0 1rem; + color: var(--muted); + font-size: 0.9rem; +} + +.app-footer { + text-align: center; + padding: 1.5rem 1rem 2rem; + color: var(--muted); +} + +@media (max-width: 480px) { + .form-grid { + grid-template-columns: 1fr; + } + + .task-item { + grid-template-columns: auto 1fr; + } + + .task-actions { + grid-column: 1 / -1; + justify-content: flex-end; + } +}