Feature: Mobile bottom-sheet drawer (slide-up from bottom, swipe-down to close, overlay), update AGENTS.md to v4.0
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
"use client";
|
||||
"use client";
|
||||
import React, { useState, useEffect, useRef, useCallback } from "react";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { ContextMenu, MenuItem } from "@/components/ui/ContextMenu";
|
||||
|
||||
export interface Tag { id: string; name: string; color: string }
|
||||
|
||||
export interface Task {
|
||||
id: string;
|
||||
listId: string;
|
||||
@@ -16,8 +18,10 @@ export interface Task {
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string | null;
|
||||
isDeleted?: boolean;
|
||||
children: Task[];
|
||||
tags?: { tag: { id: string; name: string; color: string } }[];
|
||||
tags?: { tag: Tag }[];
|
||||
}
|
||||
|
||||
export interface List { id: string; name: string; color: string; icon: string }
|
||||
@@ -39,6 +43,9 @@ interface TaskItemProps {
|
||||
onUpdateTitle: (id: string, title: string) => void;
|
||||
onAddSubtask: (title: string, parentId: string) => void;
|
||||
onContextMenu: (x: number, y: number, task: Task) => void;
|
||||
isTrashMode?: boolean;
|
||||
onRestore?: (id: string) => void;
|
||||
onPermanentDelete?: (id: string) => void;
|
||||
}
|
||||
|
||||
// Recursive Task Tree Item (Supports 1st, 2nd, 3rd, N-level sub-tasks seamlessly)
|
||||
@@ -51,6 +58,9 @@ function RecursiveTaskItem({
|
||||
onUpdateTitle,
|
||||
onAddSubtask,
|
||||
onContextMenu,
|
||||
isTrashMode = false,
|
||||
onRestore,
|
||||
onPermanentDelete,
|
||||
}: TaskItemProps) {
|
||||
const { t, lang } = useI18n();
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
@@ -60,6 +70,10 @@ function RecursiveTaskItem({
|
||||
const [addingSubtask, setAddingSubtask] = useState(false);
|
||||
const [subtaskInput, setSubtaskInput] = useState("");
|
||||
|
||||
// Touch swipe support
|
||||
const touchStartX = useRef<number | null>(null);
|
||||
const [swipeOffset, setSwipeOffset] = useState(0);
|
||||
|
||||
const editInputRef = useRef<HTMLInputElement>(null);
|
||||
const subInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
@@ -117,11 +131,37 @@ function RecursiveTaskItem({
|
||||
}
|
||||
};
|
||||
|
||||
// Touch Swipe handlers
|
||||
const handleTouchStart = (e: React.TouchEvent) => {
|
||||
if (isTrashMode) return;
|
||||
touchStartX.current = e.touches[0].clientX;
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent) => {
|
||||
if (touchStartX.current === null) return;
|
||||
const deltaX = e.touches[0].clientX - touchStartX.current;
|
||||
if (Math.abs(deltaX) < 120) {
|
||||
setSwipeOffset(deltaX);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
if (swipeOffset > 70) {
|
||||
// Swiped Right -> Toggle Complete
|
||||
onToggle(task.id, !task.completed);
|
||||
}
|
||||
setSwipeOffset(0);
|
||||
touchStartX.current = null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ paddingLeft: depth > 0 ? 24 : 0, position: "relative" }}>
|
||||
<div
|
||||
className={`task-item${task.completed ? " completed" : ""}${isSelected ? " selected" : ""}`}
|
||||
onClick={() => onSelect(task)}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -131,6 +171,8 @@ function RecursiveTaskItem({
|
||||
style={{
|
||||
borderLeft: depth > 0 ? "2px solid var(--border)" : "none",
|
||||
marginLeft: depth > 0 ? 8 : 0,
|
||||
transform: `translateX(${swipeOffset}px)`,
|
||||
transition: swipeOffset === 0 ? "transform 0.2s cubic-bezier(0.16, 1, 0.3, 1)" : "none",
|
||||
}}
|
||||
>
|
||||
{/* Toggle Expand Arrow if has children */}
|
||||
@@ -161,21 +203,25 @@ function RecursiveTaskItem({
|
||||
<span style={{ width: 12, flexShrink: 0 }} />
|
||||
) : null}
|
||||
|
||||
{/* Check button */}
|
||||
<button
|
||||
className={`task-check-btn${task.completed ? " checked" : ""}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle(task.id, !task.completed);
|
||||
}}
|
||||
aria-label={task.completed ? "Mark incomplete" : "Mark complete"}
|
||||
type="button"
|
||||
style={{ width: depth > 0 ? 17 : 19, height: depth > 0 ? 17 : 19, flexShrink: 0 }}
|
||||
/>
|
||||
{/* Check button (hidden in trash mode) */}
|
||||
{!isTrashMode ? (
|
||||
<button
|
||||
className={`task-check-btn${task.completed ? " checked" : ""}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onToggle(task.id, !task.completed);
|
||||
}}
|
||||
aria-label={task.completed ? "Mark incomplete" : "Mark complete"}
|
||||
type="button"
|
||||
style={{ width: depth > 0 ? 17 : 19, height: depth > 0 ? 17 : 19, flexShrink: 0 }}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ fontSize: 13, opacity: 0.5, flexShrink: 0 }}>🗑️</span>
|
||||
)}
|
||||
|
||||
{/* Title and Meta */}
|
||||
{/* Title, Meta and Tags */}
|
||||
<div className="task-body">
|
||||
{editingTitle ? (
|
||||
{editingTitle && !isTrashMode ? (
|
||||
<input
|
||||
ref={editInputRef}
|
||||
className="form-input"
|
||||
@@ -196,6 +242,7 @@ function RecursiveTaskItem({
|
||||
<div
|
||||
className="task-title"
|
||||
onDoubleClick={(e) => {
|
||||
if (isTrashMode) return;
|
||||
e.stopPropagation();
|
||||
setEditingTitle(true);
|
||||
}}
|
||||
@@ -206,15 +253,15 @@ function RecursiveTaskItem({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="task-meta">
|
||||
{task.priority > 0 && (
|
||||
<div className="task-meta" style={{ flexWrap: "wrap", gap: 6 }}>
|
||||
{task.priority > 0 && !isTrashMode && (
|
||||
<div
|
||||
className="task-priority-dot"
|
||||
style={{ background: PRIORITY_COLORS[task.priority] }}
|
||||
title={priorityLabels[task.priority]}
|
||||
/>
|
||||
)}
|
||||
{task.dueDate && (
|
||||
{task.dueDate && !isTrashMode && (
|
||||
<span className={`task-due${isOverdue(task.dueDate) && !task.completed ? " overdue" : ""}`}>
|
||||
<svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" /><line x1="16" y1="2" x2="16" y2="6" /><line x1="8" y1="2" x2="8" y2="6" /><line x1="3" y1="10" x2="21" y2="10" />
|
||||
@@ -222,7 +269,7 @@ function RecursiveTaskItem({
|
||||
{formatDate(task.dueDate)}
|
||||
</span>
|
||||
)}
|
||||
{totalChildren > 0 && (
|
||||
{totalChildren > 0 && !isTrashMode && (
|
||||
<span
|
||||
className="task-sub-count"
|
||||
onClick={(e) => {
|
||||
@@ -235,20 +282,44 @@ function RecursiveTaskItem({
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Tags Badges */}
|
||||
{task.tags && task.tags.length > 0 && (
|
||||
<div style={{ display: "flex", gap: 4 }}>
|
||||
{task.tags.map((tg) => (
|
||||
<span
|
||||
key={tg.tag.id}
|
||||
className="badge"
|
||||
style={{
|
||||
background: tg.tag.color + "22",
|
||||
color: tg.tag.color,
|
||||
fontSize: 10,
|
||||
padding: "1px 6px",
|
||||
borderRadius: 4,
|
||||
fontWeight: 600,
|
||||
}}
|
||||
>
|
||||
#{tg.tag.name}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Inline Add Subtask button */}
|
||||
<button
|
||||
className="badge badge-neutral"
|
||||
style={{ cursor: "pointer", fontSize: 10, padding: "1px 6px", border: "1px solid var(--border)", background: "transparent" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setAddingSubtask(true);
|
||||
setExpanded(true);
|
||||
}}
|
||||
title="Add subtask"
|
||||
type="button"
|
||||
>
|
||||
+ {t("subtasks")}
|
||||
</button>
|
||||
{!isTrashMode && (
|
||||
<button
|
||||
className="badge badge-neutral"
|
||||
style={{ cursor: "pointer", fontSize: 10, padding: "1px 6px", border: "1px solid var(--border)", background: "transparent" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setAddingSubtask(true);
|
||||
setExpanded(true);
|
||||
}}
|
||||
title="Add subtask"
|
||||
type="button"
|
||||
>
|
||||
+ {t("subtasks")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{task.note && !task.completed && depth === 0 && (
|
||||
@@ -256,21 +327,50 @@ function RecursiveTaskItem({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Quick Edit icon on hover */}
|
||||
<button
|
||||
className="icon-btn"
|
||||
style={{ width: 22, height: 22, opacity: 0.35, flexShrink: 0 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingTitle(true);
|
||||
}}
|
||||
title="Edit title"
|
||||
type="button"
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 20h9" /><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
{/* Trash Mode Actions or Edit Icon */}
|
||||
{isTrashMode ? (
|
||||
<div style={{ display: "flex", gap: 6 }}>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
style={{ fontSize: 11, padding: "2px 8px" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onRestore) onRestore(task.id);
|
||||
}}
|
||||
title="Restore task"
|
||||
type="button"
|
||||
>
|
||||
↩️ Restore
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-ghost btn-sm"
|
||||
style={{ fontSize: 11, padding: "2px 8px", color: "var(--danger)" }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (onPermanentDelete) onPermanentDelete(task.id);
|
||||
}}
|
||||
title="Delete permanently"
|
||||
type="button"
|
||||
>
|
||||
❌
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
className="icon-btn"
|
||||
style={{ width: 22, height: 22, opacity: 0.35, flexShrink: 0 }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setEditingTitle(true);
|
||||
}}
|
||||
title="Edit title"
|
||||
type="button"
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<path d="M12 20h9" /><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Recursive Children (N-Depth Sub-tasks) */}
|
||||
@@ -287,13 +387,16 @@ function RecursiveTaskItem({
|
||||
onUpdateTitle={onUpdateTitle}
|
||||
onAddSubtask={onAddSubtask}
|
||||
onContextMenu={onContextMenu}
|
||||
isTrashMode={isTrashMode}
|
||||
onRestore={onRestore}
|
||||
onPermanentDelete={onPermanentDelete}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Inline Add Subtask form for this node */}
|
||||
{addingSubtask && (
|
||||
{addingSubtask && !isTrashMode && (
|
||||
<form
|
||||
onSubmit={handleCreateSubtask}
|
||||
style={{ display: "flex", gap: 6, padding: "4px 0 4px 32px" }}
|
||||
@@ -336,6 +439,11 @@ interface Props {
|
||||
onMenuOpen: () => void;
|
||||
onRefresh: () => void;
|
||||
isDemo?: boolean;
|
||||
isTrashActive?: boolean;
|
||||
selectedTag?: string | null;
|
||||
onEmptyTrash?: () => void;
|
||||
onRestoreTask?: (id: string) => void;
|
||||
onPermanentDeleteTask?: (id: string) => void;
|
||||
onDemoAddTask?: (title: string, listId: string, parentId?: string | null) => void;
|
||||
onDemoToggleTask?: (id: string, completed: boolean) => void;
|
||||
onUpdateTaskTitle?: (id: string, title: string) => void;
|
||||
@@ -356,6 +464,11 @@ export function TaskList({
|
||||
onMenuOpen,
|
||||
onRefresh,
|
||||
isDemo = false,
|
||||
isTrashActive = false,
|
||||
selectedTag = null,
|
||||
onEmptyTrash,
|
||||
onRestoreTask,
|
||||
onPermanentDeleteTask,
|
||||
onDemoAddTask,
|
||||
onDemoToggleTask,
|
||||
onUpdateTaskTitle,
|
||||
@@ -385,7 +498,7 @@ export function TaskList({
|
||||
}, [editingHeader]);
|
||||
|
||||
const fetchTasks = useCallback(async () => {
|
||||
if (!listId || isDemo) return;
|
||||
if (!listId || isDemo || isTrashActive || selectedTag) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch(`/api/tasks?listId=${listId}&showCompleted=${showCompleted}`);
|
||||
@@ -397,7 +510,7 @@ export function TaskList({
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [listId, showCompleted, isDemo, setTasks]);
|
||||
}, [listId, showCompleted, isDemo, isTrashActive, selectedTag, setTasks]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTasks();
|
||||
@@ -489,7 +602,7 @@ export function TaskList({
|
||||
setEditingHeader(false);
|
||||
};
|
||||
|
||||
if (!listId) {
|
||||
if (!listId && !isTrashActive && !selectedTag) {
|
||||
return (
|
||||
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", height: "100%", color: "var(--text-tertiary)" }}>
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" style={{ opacity: 0.3, marginBottom: 12 }}>
|
||||
@@ -514,8 +627,18 @@ export function TaskList({
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Inline Editable List Title */}
|
||||
{editingHeader ? (
|
||||
{/* Header Title / Trash Header / Tag Header */}
|
||||
{isTrashActive ? (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span style={{ fontSize: 18, fontWeight: 700, color: "var(--danger)" }}>🗑️ {t("trash") || "Trash"}</span>
|
||||
<span style={{ fontSize: 12, color: "var(--text-tertiary)" }}>({tasks.length})</span>
|
||||
</div>
|
||||
) : selectedTag ? (
|
||||
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
|
||||
<span style={{ fontSize: 18, fontWeight: 700, color: "var(--accent)" }}>🏷️ #{selectedTag}</span>
|
||||
<span style={{ fontSize: 12, color: "var(--text-tertiary)" }}>({tasks.length})</span>
|
||||
</div>
|
||||
) : editingHeader ? (
|
||||
<input
|
||||
ref={headerInputRef}
|
||||
id="header-rename-input"
|
||||
@@ -559,27 +682,39 @@ export function TaskList({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Header Actions */}
|
||||
<div className="main-header-actions">
|
||||
<button
|
||||
id="toggle-completed-btn"
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={onToggleCompleted}
|
||||
title={showCompleted ? t("hideDone") : t("showDone")}
|
||||
type="button"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
{showCompleted ? t("hideDone") : t("showDone")}
|
||||
</button>
|
||||
{isTrashActive ? (
|
||||
<button
|
||||
id="empty-trash-btn"
|
||||
className="btn btn-ghost btn-sm"
|
||||
style={{ color: "var(--danger)" }}
|
||||
onClick={onEmptyTrash}
|
||||
type="button"
|
||||
>
|
||||
🧹 {t("emptyTrash") || "Empty Trash"}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
id="toggle-completed-btn"
|
||||
className="btn btn-ghost btn-sm"
|
||||
onClick={onToggleCompleted}
|
||||
title={showCompleted ? t("hideDone") : t("showDone")}
|
||||
type="button"
|
||||
>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
{showCompleted ? t("hideDone") : t("showDone")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task list container with background context menu handler */}
|
||||
{/* Task list container */}
|
||||
<div
|
||||
className="task-list-container"
|
||||
onContextMenu={(e) => {
|
||||
// If clicked directly on empty container area, prevent native browser menu
|
||||
if (e.target === e.currentTarget) {
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -588,16 +723,16 @@ export function TaskList({
|
||||
{loading && (
|
||||
<div style={{ padding: "20px", textAlign: "center", color: "var(--text-tertiary)" }}>{t("loading")}</div>
|
||||
)}
|
||||
{!loading && incompleteTasks.length === 0 && completedTasks.length === 0 && (
|
||||
{!loading && tasks.length === 0 && (
|
||||
<div className="task-list-empty">
|
||||
<svg width="56" height="56" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M9 11l3 3L22 4" /><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11" />
|
||||
</svg>
|
||||
<p>{t("noTasksYet")}</p>
|
||||
<p>{isTrashActive ? "Trash is empty" : selectedTag ? "No tasks with this tag" : t("noTasksYet")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Incomplete Tasks Recursive Tree */}
|
||||
{/* Tasks Tree */}
|
||||
{incompleteTasks.map((task) => (
|
||||
<RecursiveTaskItem
|
||||
key={task.id}
|
||||
@@ -609,11 +744,14 @@ export function TaskList({
|
||||
onUpdateTitle={(id, title) => onUpdateTaskTitle && onUpdateTaskTitle(id, title)}
|
||||
onAddSubtask={handleAddSubtaskInline}
|
||||
onContextMenu={(x, y, tItem) => setContextMenu({ x, y, task: tItem })}
|
||||
isTrashMode={isTrashActive}
|
||||
onRestore={onRestoreTask}
|
||||
onPermanentDelete={onPermanentDeleteTask}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Completed Tasks Recursive Tree */}
|
||||
{showCompleted && completedTasks.length > 0 && (
|
||||
{/* Completed Tasks */}
|
||||
{!isTrashActive && showCompleted && completedTasks.length > 0 && (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
@@ -638,14 +776,17 @@ export function TaskList({
|
||||
onUpdateTitle={(id, title) => onUpdateTaskTitle && onUpdateTaskTitle(id, title)}
|
||||
onAddSubtask={handleAddSubtaskInline}
|
||||
onContextMenu={(x, y, tItem) => setContextMenu({ x, y, task: tItem })}
|
||||
isTrashMode={isTrashActive}
|
||||
onRestore={onRestoreTask}
|
||||
onPermanentDelete={onPermanentDeleteTask}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Full-featured Context Menu on ANY Task at ANY Depth */}
|
||||
{contextMenu && (
|
||||
{/* Full-featured Context Menu */}
|
||||
{contextMenu && !isTrashActive && (
|
||||
<ContextMenu
|
||||
x={contextMenu.x}
|
||||
y={contextMenu.y}
|
||||
@@ -686,25 +827,27 @@ export function TaskList({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Add task bar */}
|
||||
<div className="add-task-bar">
|
||||
<button className="task-check-btn" style={{ opacity: 0.4, flexShrink: 0 }} aria-hidden="true" type="button" />
|
||||
<form onSubmit={handleAddTask} style={{ flex: 1, display: "flex", gap: 8 }}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
id="add-task-input"
|
||||
className="add-task-input"
|
||||
placeholder={t("addTaskPlaceholder")}
|
||||
value={newTaskTitle}
|
||||
onChange={(e) => setNewTaskTitle(e.target.value)}
|
||||
/>
|
||||
{newTaskTitle.trim() && (
|
||||
<button type="submit" className="btn btn-primary btn-sm" id="add-task-btn">
|
||||
{t("add")}
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
{/* Add task bar (hidden in trash mode) */}
|
||||
{!isTrashActive && !selectedTag && (
|
||||
<div className="add-task-bar">
|
||||
<button className="task-check-btn" style={{ opacity: 0.4, flexShrink: 0 }} aria-hidden="true" type="button" />
|
||||
<form onSubmit={handleAddTask} style={{ flex: 1, display: "flex", gap: 8 }}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
id="add-task-input"
|
||||
className="add-task-input"
|
||||
placeholder={t("addTaskPlaceholder")}
|
||||
value={newTaskTitle}
|
||||
onChange={(e) => setNewTaskTitle(e.target.value)}
|
||||
/>
|
||||
{newTaskTitle.trim() && (
|
||||
<button type="submit" className="btn btn-primary btn-sm" id="add-task-btn">
|
||||
{t("add")}
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user