1- // DocsListPage.tsx
2- import React , { useEffect , useState } from "react" ;
1+ import React , { useEffect , useState , useMemo } from "react" ;
32import { Link } from "react-router-dom" ;
43import { useApiClient } from "../services/apiClient" ;
5- import { useAuthContextSwitch as useAuthContext } from "../context/useAuthContextSwitch" ;
4+ import { useAuthContextSwitch as useAuthContext } from "../context/useAuthContextSwitch" ;
65import { AxiosError } from "axios" ;
7-
86import styles from "../styles/DocsListPage.module.scss" ;
97
108const extractTitle = ( markdown : string ) : string => {
@@ -20,6 +18,14 @@ const extractTitle = (markdown: string): string => {
2018const DocsListPage : React . FC = ( ) => {
2119 const [ documents , setDocuments ] = useState < any [ ] > ( [ ] ) ; //eslint-disable-line
2220 const [ error , setError ] = useState < string | null > ( null ) ;
21+
22+ // ★ 検索用ステートを追加
23+ const [ searchTerm , setSearchTerm ] = useState ( "" ) ;
24+
25+ // ★ ページング用ステート
26+ const [ currentPage , setCurrentPage ] = useState ( 1 ) ;
27+ const pageSize = 15 ; // 1ページあたりの表示件数
28+
2329 const { user, isSignedIn } = useAuthContext ( ) ;
2430 const api = useApiClient ( ) ;
2531
@@ -48,14 +54,42 @@ const DocsListPage: React.FC = () => {
4854 }
4955 }
5056 } ;
57+
5158 if ( isSignedIn ) {
5259 fetchDocs ( ) ;
5360 }
5461 } , [ api , user , isSignedIn ] ) ;
5562
56- if ( error ) {
57- return < div style = { { color : "red" } } > { error } </ div > ;
58- }
63+ // ★ 1) 検索フィルタリング:searchTerm を含むドキュメントだけを抽出
64+ const filteredDocs = useMemo ( ( ) => {
65+ const lowerSearchTerm = searchTerm . toLowerCase ( ) ;
66+ if ( ! lowerSearchTerm ) return documents ;
67+
68+ return documents . filter ( ( doc ) => {
69+ const title = extractTitle ( doc . content ) . toLowerCase ( ) ;
70+ const content = doc . content ?. toLowerCase ( ) || "" ;
71+ return (
72+ title . includes ( lowerSearchTerm ) || content . includes ( lowerSearchTerm )
73+ ) ;
74+ } ) ;
75+ } , [ documents , searchTerm ] ) ;
76+
77+ // ★ 2) ページング
78+ const totalPages = Math . ceil ( filteredDocs . length / pageSize ) ;
79+
80+ const paginatedDocs = useMemo ( ( ) => {
81+ const startIndex = ( currentPage - 1 ) * pageSize ;
82+ const endIndex = startIndex + pageSize ;
83+ return filteredDocs . slice ( startIndex , endIndex ) ;
84+ } , [ filteredDocs , currentPage ] ) ;
85+
86+ // 前へ/次へボタンの挙動
87+ const goToPrevious = ( ) => {
88+ setCurrentPage ( ( prev ) => Math . max ( prev - 1 , 1 ) ) ;
89+ } ;
90+ const goToNext = ( ) => {
91+ setCurrentPage ( ( prev ) => Math . min ( prev + 1 , totalPages ) ) ;
92+ } ;
5993
6094 // 「共有」ボタン押下時の挙動例
6195 const handleShare = ( slug : string ) => {
@@ -64,12 +98,15 @@ const DocsListPage: React.FC = () => {
6498 alert ( "公開URLをクリップボードにコピーしました!\n" + publicUrl ) ;
6599 } ;
66100
101+ if ( error ) {
102+ return < div style = { { color : "red" } } > { error } </ div > ;
103+ }
104+
67105 return (
68- < div style = { { width : "100%" , maxWidth : "900px" , margin : "10px auto" , textAlign : "left" } } >
106+ < div style = { { width : "100%" , maxWidth : "900px" , margin : "10px auto" } } >
69107 { isSignedIn ? (
70108 < >
71109 < div style = { { margin : "16px 0" , textAlign : "right" } } >
72- { /* 新規ドキュメントは "/" に */ }
73110 < Link
74111 to = "/"
75112 style = { {
@@ -86,6 +123,25 @@ const DocsListPage: React.FC = () => {
86123
87124 < h1 > ドキュメント一覧</ h1 >
88125
126+ { /* ★ 検索入力欄 */ }
127+ < div style = { { margin : "16px 0" } } >
128+ < input
129+ type = "text"
130+ placeholder = "検索キーワードを入力"
131+ value = { searchTerm }
132+ onChange = { ( e ) => {
133+ setSearchTerm ( e . target . value ) ;
134+ setCurrentPage ( 1 ) ; // 検索語が変わったら1ページ目に戻す
135+ } }
136+ style = { {
137+ padding : "8px" ,
138+ borderRadius : "4px" ,
139+ border : "1px solid #ccc" ,
140+ width : "240px" ,
141+ } }
142+ />
143+ </ div >
144+
89145 < table className = { styles . docsTable } >
90146 < thead >
91147 < tr style = { { backgroundColor : "#f0f0f0" , borderBottom : "2px solid #ccc" } } >
@@ -99,12 +155,11 @@ const DocsListPage: React.FC = () => {
99155 </ tr >
100156 </ thead >
101157 < tbody >
102- { documents . map ( ( doc ) => {
158+ { paginatedDocs . map ( ( doc ) => {
103159 const title = extractTitle ( doc . content ) ;
104160 const isPublic = doc . isPublic ;
105161 return (
106162 < tr key = { doc . slug } style = { { borderBottom : "1px solid #ddd" } } >
107- { /* タイトル */ }
108163 < td style = { { padding : "8px" } } >
109164 < Link
110165 to = { `/my-docs/${ doc . slug } ` }
@@ -113,20 +168,19 @@ const DocsListPage: React.FC = () => {
113168 < strong > { title } </ strong >
114169 </ Link >
115170 </ td >
116-
117- { /* 公開/非公開 */ }
118171 < td style = { { padding : "8px" } } >
119172 { isPublic ? (
120173 < span style = { { color : "green" } } > 公開</ span >
121174 ) : (
122175 < span style = { { color : "gray" } } > 非公開</ span >
123176 ) }
124177 </ td >
125-
126- { /* 共有ボタン(公開時のみ) */ }
127178 < td style = { { padding : "8px" } } >
128179 { isPublic ? (
129- < button className = { styles . shareButton } onClick = { ( ) => handleShare ( doc . slug ) } >
180+ < button
181+ className = { styles . shareButton }
182+ onClick = { ( ) => handleShare ( doc . slug ) }
183+ >
130184 共有
131185 </ button >
132186 ) : (
@@ -138,6 +192,25 @@ const DocsListPage: React.FC = () => {
138192 } ) }
139193 </ tbody >
140194 </ table >
195+
196+ { /* ★ ページングナビゲーション */ }
197+ { filteredDocs . length > 0 && totalPages > 1 && (
198+ < div style = { { marginTop : "1rem" , display : "flex" , gap : "8px" } } >
199+ { /* 前へボタン: currentPage>1 のときだけ表示 */ }
200+ { currentPage > 1 && (
201+ < button onClick = { goToPrevious } > 前へ</ button >
202+ ) }
203+
204+ < div style = { { lineHeight : "32px" } } >
205+ ページ { currentPage } / { totalPages }
206+ </ div >
207+
208+ { /* 次へボタン: currentPage<totalPages のときだけ表示 */ }
209+ { currentPage < totalPages && (
210+ < button onClick = { goToNext } > 次へ</ button >
211+ ) }
212+ </ div >
213+ ) }
141214 </ >
142215 ) : (
143216 < div > ログインしてください</ div >
0 commit comments