@@ -4,6 +4,8 @@ import React, {
44 useReducer ,
55 useState ,
66 startTransition ,
7+ useCallback ,
8+ useRef ,
79} from "react" ;
810import { Button } from "@/components/ui/button" ;
911import { 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