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
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
- **フロントエンド**: Vite + React をベースに、Markdownエディタや認証機能、UI等を提供
- **バックエンド**: AWS Lambda (Node.js) + API Gateway + DynamoDB のサーバーレス構成

## デモアプリケーションイメージ
### (認証不要)Markdownエディタ画面
![image](https://github.com/user-attachments/assets/c41ac2b7-c24a-405d-9653-bf599715891b)

### (要認証)ドキュメント一覧画面
![image](https://github.com/user-attachments/assets/99ea0435-ed2c-40a2-abad-9ec9094ea55a)

### (認証不要)公開ドキュメント閲覧画面
![](https://github.com/user-attachments/assets/4d0d0eb4-7aae-45d1-97d7-93aed02a5491)

## 主な特徴

- **Markdown ドキュメントの作成・編集・公開**
Expand Down
105 changes: 89 additions & 16 deletions frontend/src/pages/DocsListPage.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
// DocsListPage.tsx
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useMemo } from "react";
import { Link } from "react-router-dom";
import { useApiClient } from "../services/apiClient";
import { useAuthContextSwitch as useAuthContext} from "../context/useAuthContextSwitch";
import { useAuthContextSwitch as useAuthContext } from "../context/useAuthContextSwitch";
import { AxiosError } from "axios";

import styles from "../styles/DocsListPage.module.scss";

const extractTitle = (markdown: string): string => {
Expand All @@ -20,6 +18,14 @@ const extractTitle = (markdown: string): string => {
const DocsListPage: React.FC = () => {
const [documents, setDocuments] = useState<any[]>([]); //eslint-disable-line
const [error, setError] = useState<string | null>(null);

// ★ 検索用ステートを追加
const [searchTerm, setSearchTerm] = useState("");

// ★ ページング用ステート
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 15; // 1ページあたりの表示件数

const { user, isSignedIn } = useAuthContext();
const api = useApiClient();

Expand Down Expand Up @@ -48,14 +54,42 @@ const DocsListPage: React.FC = () => {
}
}
};

if (isSignedIn) {
fetchDocs();
}
}, [api, user, isSignedIn]);

if (error) {
return <div style={{ color: "red" }}>{error}</div>;
}
// ★ 1) 検索フィルタリング:searchTerm を含むドキュメントだけを抽出
const filteredDocs = useMemo(() => {
const lowerSearchTerm = searchTerm.toLowerCase();
if (!lowerSearchTerm) return documents;

return documents.filter((doc) => {
const title = extractTitle(doc.content).toLowerCase();
const content = doc.content?.toLowerCase() || "";
return (
title.includes(lowerSearchTerm) || content.includes(lowerSearchTerm)
);
});
}, [documents, searchTerm]);

// ★ 2) ページング
const totalPages = Math.ceil(filteredDocs.length / pageSize);

const paginatedDocs = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return filteredDocs.slice(startIndex, endIndex);
}, [filteredDocs, currentPage]);

// 前へ/次へボタンの挙動
const goToPrevious = () => {
setCurrentPage((prev) => Math.max(prev - 1, 1));
};
const goToNext = () => {
setCurrentPage((prev) => Math.min(prev + 1, totalPages));
};

// 「共有」ボタン押下時の挙動例
const handleShare = (slug: string) => {
Expand All @@ -64,12 +98,15 @@ const DocsListPage: React.FC = () => {
alert("公開URLをクリップボードにコピーしました!\n" + publicUrl);
};

if (error) {
return <div style={{ color: "red" }}>{error}</div>;
}

return (
<div style={{ width: "100%", maxWidth: "900px", margin: "10px auto", textAlign: "left" }}>
<div style={{ width: "100%", maxWidth: "900px", margin: "10px auto" }}>
{isSignedIn ? (
<>
<div style={{ margin: "16px 0", textAlign: "right" }}>
{/* 新規ドキュメントは "/" に */}
<Link
to="/"
style={{
Expand All @@ -86,6 +123,25 @@ const DocsListPage: React.FC = () => {

<h1>ドキュメント一覧</h1>

{/* ★ 検索入力欄 */}
<div style={{ margin: "16px 0" }}>
<input
type="text"
placeholder="検索キーワードを入力"
value={searchTerm}
onChange={(e) => {
setSearchTerm(e.target.value);
setCurrentPage(1); // 検索語が変わったら1ページ目に戻す
}}
style={{
padding: "8px",
borderRadius: "4px",
border: "1px solid #ccc",
width: "240px",
}}
/>
</div>

<table className={styles.docsTable}>
<thead>
<tr style={{ backgroundColor: "#f0f0f0", borderBottom: "2px solid #ccc" }}>
Expand All @@ -99,12 +155,11 @@ const DocsListPage: React.FC = () => {
</tr>
</thead>
<tbody>
{documents.map((doc) => {
{paginatedDocs.map((doc) => {
const title = extractTitle(doc.content);
const isPublic = doc.isPublic;
return (
<tr key={doc.slug} style={{ borderBottom: "1px solid #ddd" }}>
{/* タイトル */}
<td style={{ padding: "8px" }}>
<Link
to={`/my-docs/${doc.slug}`}
Expand All @@ -113,20 +168,19 @@ const DocsListPage: React.FC = () => {
<strong>{title}</strong>
</Link>
</td>

{/* 公開/非公開 */}
<td style={{ padding: "8px" }}>
{isPublic ? (
<span style={{ color: "green" }}>公開</span>
) : (
<span style={{ color: "gray" }}>非公開</span>
)}
</td>

{/* 共有ボタン(公開時のみ) */}
<td style={{ padding: "8px" }}>
{isPublic ? (
<button className={styles.shareButton} onClick={() => handleShare(doc.slug)}>
<button
className={styles.shareButton}
onClick={() => handleShare(doc.slug)}
>
共有
</button>
) : (
Expand All @@ -138,6 +192,25 @@ const DocsListPage: React.FC = () => {
})}
</tbody>
</table>

{/* ★ ページングナビゲーション */}
{filteredDocs.length > 0 && totalPages > 1 && (
<div style={{ marginTop: "1rem", display: "flex", gap: "8px" }}>
{/* 前へボタン: currentPage>1 のときだけ表示 */}
{currentPage > 1 && (
<button onClick={goToPrevious}>前へ</button>
)}

<div style={{ lineHeight: "32px" }}>
ページ {currentPage} / {totalPages}
</div>

{/* 次へボタン: currentPage<totalPages のときだけ表示 */}
{currentPage < totalPages && (
<button onClick={goToNext}>次へ</button>
)}
</div>
)}
</>
) : (
<div>ログインしてください</div>
Expand Down
Loading