Skip to content

Commit 0c296eb

Browse files
committed
feat: Implement task name suggestions with debounce for improved user experience
1 parent 4b571be commit 0c296eb

File tree

1 file changed

+151
-8
lines changed

1 file changed

+151
-8
lines changed

ui/src/pages/TaskList/TaskList.tsx

Lines changed: 151 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import React, {
44
useReducer,
55
useState,
66
startTransition,
7+
useCallback,
8+
useRef,
79
} from "react";
810
import { Button } from "@/components/ui/button";
911
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -52,13 +54,54 @@ const TaskList = () => {
5254
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
5355
new Date(),
5456
);
57+
const [taskSuggestions, setTaskSuggestions] = useState<string[]>([]);
58+
const [showSuggestions, setShowSuggestions] = useState(false);
59+
const [selectedSuggestionIndex, setSelectedSuggestionIndex] = useState(-1);
60+
const debounceTimeoutRef = useRef<number | null>(null);
5561

5662
const { toast } = useToast();
5763

5864
// フックを使用
5965
const taskEdit = useTaskEdit(dispatch);
6066
const taskActions = useTaskActions(dispatch);
6167

68+
// 過去のタスク名を検索
69+
const searchTaskNames = useCallback(async (query: string) => {
70+
if (!query.trim() || !user?.id) {
71+
setTaskSuggestions([]);
72+
setShowSuggestions(false);
73+
return;
74+
}
75+
76+
const { data, error } = await supabase
77+
.from("tasks")
78+
.select("title")
79+
.eq("user_id", user.id)
80+
.ilike("title", `%${query}%`)
81+
.neq("title", query)
82+
.order("created_at", { ascending: false })
83+
.limit(5);
84+
85+
if (error) {
86+
console.error("Error searching task names:", error);
87+
return;
88+
}
89+
90+
const uniqueTitles = Array.from(new Set(data?.map(task => task.title as string) || []));
91+
setTaskSuggestions(uniqueTitles);
92+
setShowSuggestions(uniqueTitles.length > 0);
93+
}, [user?.id]);
94+
95+
// デバウンス付きの検索
96+
const debouncedSearch = useCallback((query: string) => {
97+
if (debounceTimeoutRef.current) {
98+
clearTimeout(debounceTimeoutRef.current);
99+
}
100+
debounceTimeoutRef.current = window.setTimeout(() => {
101+
searchTaskNames(query);
102+
}, 300);
103+
}, [searchTaskNames]);
104+
62105
// 最終タスクの終了時間を取得
63106
// tasksのうち、最も終了時間が遅いタスクの終了時間を取得
64107
const lastTaskEndTime = tasks.reduce<string | null>((latest, task) => {
@@ -222,13 +265,86 @@ const TaskList = () => {
222265
});
223266
};
224267

268+
// 提案を選択
269+
const selectSuggestion = async (suggestion: string) => {
270+
setShowSuggestions(false);
271+
setSelectedSuggestionIndex(-1);
272+
setNewTaskTitle("");
273+
274+
// 提案されたタスク名で直接タスクを追加
275+
if (!selectedDate || !user?.id) return;
276+
277+
startTransition(async () => {
278+
const newTask = {
279+
title: suggestion,
280+
description: "",
281+
user_id: user.id,
282+
estimated_minute: null,
283+
task_date: convertDateStringToDate(
284+
selectedDate
285+
.toLocaleString("ja-JP", { timeZone: "Asia/Tokyo" })
286+
.split(" ")[0],
287+
),
288+
};
289+
290+
const { data, error } = await supabase
291+
.from("tasks")
292+
.insert(newTask)
293+
.select();
294+
295+
if (error) {
296+
toast({
297+
title: "Error",
298+
description: "Failed to add task",
299+
variant: "destructive",
300+
});
301+
console.error("Error adding task:", error);
302+
} else {
303+
dispatch({
304+
type: "ADD_TASK",
305+
payload: (data?.[0] as unknown as Task) || ({} as Task),
306+
});
307+
}
308+
});
309+
};
310+
225311
// キー入力イベントを処理
226312
const handleNewTaskKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
227-
if (e.key === "Enter" && !e.nativeEvent.isComposing) {
313+
if (showSuggestions) {
314+
if (e.key === "ArrowDown") {
315+
e.preventDefault();
316+
setSelectedSuggestionIndex(prev =>
317+
prev < taskSuggestions.length - 1 ? prev + 1 : 0
318+
);
319+
} else if (e.key === "ArrowUp") {
320+
e.preventDefault();
321+
setSelectedSuggestionIndex(prev =>
322+
prev > 0 ? prev - 1 : taskSuggestions.length - 1
323+
);
324+
} else if (e.key === "Enter" && !e.nativeEvent.isComposing) {
325+
e.preventDefault();
326+
if (selectedSuggestionIndex >= 0) {
327+
selectSuggestion(taskSuggestions[selectedSuggestionIndex]);
328+
} else {
329+
handleAddTask();
330+
}
331+
} else if (e.key === "Escape") {
332+
setShowSuggestions(false);
333+
setSelectedSuggestionIndex(-1);
334+
}
335+
} else if (e.key === "Enter" && !e.nativeEvent.isComposing) {
228336
handleAddTask();
229337
}
230338
};
231339

340+
// 入力変更処理
341+
const handleTaskTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
342+
const value = e.target.value;
343+
setNewTaskTitle(value);
344+
setSelectedSuggestionIndex(-1);
345+
debouncedSearch(value);
346+
};
347+
232348
// ドラッグ&ドロップの処理
233349
const handleDragEnd = async (event: DragEndEvent) => {
234350
const { active, over } = event;
@@ -311,13 +427,40 @@ const TaskList = () => {
311427
<Card className="border-dashed border-2">
312428
<CardContent className="pt-6">
313429
<div className="flex gap-3">
314-
<Input
315-
placeholder="新しいタスク名を入力"
316-
value={newTaskTitle}
317-
onChange={(e) => setNewTaskTitle(e.target.value)}
318-
onKeyDown={handleNewTaskKeyDown}
319-
className="flex-1"
320-
/>
430+
<div className="relative flex-1">
431+
<Input
432+
placeholder="新しいタスク名を入力"
433+
value={newTaskTitle}
434+
onChange={handleTaskTitleChange}
435+
onKeyDown={handleNewTaskKeyDown}
436+
className="w-full"
437+
onBlur={() => {
438+
// 少し遅延してから非表示にする(クリックイベントを処理するため)
439+
setTimeout(() => setShowSuggestions(false), 200);
440+
}}
441+
onFocus={() => {
442+
if (taskSuggestions.length > 0) {
443+
setShowSuggestions(true);
444+
}
445+
}}
446+
/>
447+
{showSuggestions && taskSuggestions.length > 0 && (
448+
<div className="absolute top-full left-0 right-0 z-50 mt-1 bg-white border border-gray-200 rounded-md shadow-lg max-h-48 overflow-y-auto">
449+
{taskSuggestions.map((suggestion, index) => (
450+
<div
451+
key={suggestion}
452+
className={cn(
453+
"px-3 py-2 cursor-pointer hover:bg-gray-100",
454+
selectedSuggestionIndex === index && "bg-blue-50 text-blue-600"
455+
)}
456+
onClick={() => selectSuggestion(suggestion)}
457+
>
458+
{suggestion}
459+
</div>
460+
))}
461+
</div>
462+
)}
463+
</div>
321464
<Button onClick={handleAddTask} disabled={!newTaskTitle.trim()}>
322465
クイック追加
323466
</Button>

0 commit comments

Comments
 (0)