Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import ForecastPage from "./pages/ForecastPage";
import ForecastExtendedPage from "./pages/ForecastExtendedPage";
import PasswordResetPage from "./pages/PasswordResetPage";
import SettingsPage from "./pages/SettingsPage";
import TagsPage from "./pages/TagsPage";

function App() {
return (
Expand All @@ -46,6 +47,7 @@ function App() {
<Route path="/password-reset" element={<PasswordResetPage />} />
<Route path="/password-reset/:token" element={<PasswordResetPage />} />
<Route path="/settings" element={<SettingsPage />} />
<Route path="/tags" element={<TagsPage />} />
</Routes>

<Footer />
Expand Down
293 changes: 0 additions & 293 deletions frontend/src/components/features/auth/TagManager.jsx
Original file line number Diff line number Diff line change
@@ -1,293 +0,0 @@
import { useState, useEffect } from "react";
import { Trash2, Plus, Loader, AlertCircle } from "lucide-react";
import { tagsService } from "../../../services/tagsService";
import "./tags.css";

const TAG_COLORS = [
"#3b82f6",
"#ef4444",
"#10b981",
"#f59e0b",
"#8b5cf6",
"#ec4899",
"#14b8a6",
"#f97316",
];

function TagManager() {
const [tags, setTags] = useState([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(null);

const [newTagName, setNewTagName] = useState("");
const [newTagColor, setNewTagColor] = useState(TAG_COLORS[0]);
const [editingId, setEditingId] = useState(null);
const [editName, setEditName] = useState("");
const [editColor, setEditColor] = useState("");

useEffect(() => {
const token = localStorage.getItem("access_token");
if (token) loadTags();
else setError("No estás autenticado. Por favor, inicia sesión.");
}, []);

const loadTags = async () => {
setLoading(true);
setError(null);
try {
const data = await tagsService.getTags();
setTags(Array.isArray(data) ? data : []);
} catch (err) {
setError(err.message || "Error al cargar etiquetas");
} finally {
setLoading(false);
}
};

const handleCreateTag = async (e) => {
e.preventDefault();
if (!newTagName.trim()) {
setError("El nombre de la etiqueta no puede estar vacío");
return;
}
setLoading(true);
setError(null);
try {
await tagsService.createTag(newTagName.trim(), newTagColor);
setSuccess("Etiqueta creada exitosamente");
setNewTagName("");
setNewTagColor(TAG_COLORS[0]);
await loadTags();
setTimeout(() => setSuccess(null), 3000);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

const handleStartEdit = (tag) => {
setEditingId(tag.id);
setEditName(tag.name);
setEditColor(tag.color);
};

const handleSaveEdit = async (tagId) => {
if (!editName.trim()) {
setError("El nombre de la etiqueta no puede estar vacío");
return;
}
setLoading(true);
setError(null);
try {
await tagsService.updateTag(tagId, editName.trim(), editColor);
setSuccess("Etiqueta actualizada exitosamente");
setEditingId(null);
await loadTags();
setTimeout(() => setSuccess(null), 3000);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

const handleCancelEdit = () => {
setEditingId(null);
setEditName("");
setEditColor("");
};

const handleDeleteTag = async (tagId) => {
if (!confirm("¿Estás seguro de que deseas eliminar esta etiqueta?")) return;
setLoading(true);
setError(null);
try {
await tagsService.deleteTag(tagId);
setSuccess("Etiqueta eliminada exitosamente");
await loadTags();
setTimeout(() => setSuccess(null), 3000);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};

return (
<div className="tag-manager-container">
<div className="tag-manager-header">
<h2>Mis Etiquetas</h2>
<p className="tag-manager-subtitle">
Gestiona tus etiquetas personalizadas
</p>
</div>

{/* Mensajes */}
{error && (
<div className="tag-manager-alert tag-manager-alert-error">
<AlertCircle size={20} />
<span>{error}</span>
<button
onClick={() => setError(null)}
className="tag-manager-alert-close"
>
×
</button>
</div>
)}

{success && (
<div className="tag-manager-alert tag-manager-alert-success">
<span>✓ {success}</span>
<button
onClick={() => setSuccess(null)}
className="tag-manager-alert-close"
>
×
</button>
</div>
)}

{/* Form */}
<form className="tag-manager-form" onSubmit={handleCreateTag}>
<div className="tag-manager-form-group">
<label className="tag-manager-label">Nombre de la etiqueta</label>
<input
type="text"
className="tag-manager-input"
placeholder="Ej: Importante, Urgente, Personal..."
value={newTagName}
onChange={(e) => setNewTagName(e.target.value)}
disabled={loading}
maxLength="50"
/>
</div>

<div className="tag-manager-form-group">
<label className="tag-manager-label">Color</label>
<div className="tag-manager-color-picker">
{TAG_COLORS.map((color) => (
<button
key={color}
type="button"
className={`tag-manager-color-option ${
newTagColor === color ? "selected" : ""
}`}
style={{ backgroundColor: color }}
onClick={() => setNewTagColor(color)}
/>
))}
</div>
</div>

<button
type="submit"
className="tag-manager-button tag-manager-button-primary"
disabled={loading || !newTagName.trim()}
>
{loading ? (
<>
<Loader size={18} className="tag-manager-spinner" /> Creando...
</>
) : (
<>
<Plus size={18} /> Crear Etiqueta
</>
)}
</button>
</form>

{/* Lista */}
<div className="tag-manager-list">
{loading && !tags.length ? (
<div className="tag-manager-loading">
<Loader className="tag-manager-spinner" size={32} />
<p>Cargando etiquetas...</p>
</div>
) : tags.length === 0 ? (
<div className="tag-manager-empty">
<p>No tienes etiquetas aún. ¡Crea una para empezar!</p>
</div>
) : (
<div className="tag-manager-grid">
{tags.map((tag) => (
<div key={tag.id} className="tag-manager-item">
{editingId === tag.id ? (
<div className="tag-manager-edit-form">
<input
type="text"
className="tag-manager-input tag-manager-input-small"
value={editName}
onChange={(e) => setEditName(e.target.value)}
maxLength="50"
/>
<div className="tag-manager-color-picker-small">
{TAG_COLORS.map((color) => (
<button
key={color}
type="button"
className={`tag-manager-color-option-small ${
editColor === color ? "selected" : ""
}`}
style={{ backgroundColor: color }}
onClick={() => setEditColor(color)}
/>
))}
</div>
<div className="tag-manager-edit-actions">
<button
className="tag-manager-button tag-manager-button-small tag-manager-button-success"
onClick={() => handleSaveEdit(tag.id)}
disabled={loading}
>
Guardar
</button>
<button
className="tag-manager-button tag-manager-button-small tag-manager-button-secondary"
onClick={handleCancelEdit}
disabled={loading}
>
Cancelar
</button>
</div>
</div>
) : (
<>
<div className="tag-manager-tag-display">
<span
className="tag-manager-tag-badge"
style={{ backgroundColor: tag.color }}
>
{tag.name}
</span>
</div>
<div className="tag-manager-tag-actions">
<button
className="tag-manager-button tag-manager-button-small tag-manager-button-secondary"
onClick={() => handleStartEdit(tag)}
disabled={loading}
>
Editar
</button>
<button
className="tag-manager-button tag-manager-button-small tag-manager-button-danger"
onClick={() => handleDeleteTag(tag.id)}
disabled={loading}
>
<Trash2 size={16} />
</button>
</div>
</>
)}
</div>
))}
</div>
)}
</div>
</div>
);
}

export default TagManager;
8 changes: 8 additions & 0 deletions frontend/src/components/features/auth/UserPanelInfo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ function UserPanelInfo() {
</div>

<div className="auth-user-actions">
<button
className="auth-button-secondary"
onClick={() => navigate('/tags')}
>
Mis etiquetas
</button>

{user && (
<button
className="auth-button-secondary"
Expand All @@ -63,6 +70,7 @@ function UserPanelInfo() {
</button>
)}
</div>
</div>
</div>
)
}
Expand Down
Loading