From 32ed23580731cff3251dec69e43e3955d7dddc73 Mon Sep 17 00:00:00 2001 From: miteruzo Date: Fri, 13 Jun 2025 01:36:53 +0900 Subject: [PATCH] =?UTF-8?q?#30=20#31=20=E5=AE=8C=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tsx | 31 +-- frontend/src/components/TagDetailSidebar.tsx | 60 ++++++ frontend/src/components/TagSidebar.tsx | 59 +++--- frontend/src/components/layout/MainArea.tsx | 9 + .../components/layout/SidebarComponent.tsx | 9 + frontend/src/pages/PostDetailPage.tsx | 88 +++++---- frontend/src/pages/PostNewPage.tsx | 180 +++++++++--------- frontend/src/pages/PostPage.tsx | 37 ++-- frontend/src/pages/TagPage.tsx | 29 +-- frontend/src/pages/WikiDetailPage.tsx | 24 +-- frontend/src/pages/WikiNewPage.tsx | 58 +++--- 11 files changed, 326 insertions(+), 258 deletions(-) create mode 100644 frontend/src/components/TagDetailSidebar.tsx create mode 100644 frontend/src/components/layout/MainArea.tsx create mode 100644 frontend/src/components/layout/SidebarComponent.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3cd0874..f978178 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,6 +3,7 @@ import { BrowserRouter as Router, Route, Routes, Navigate } from 'react-router-d import TagPage from '@/pages/TagPage' import TopNav from '@/components/TopNav' import TagSidebar from '@/components/TagSidebar' +import TagDetailSidebar from '@/components/TagDetailSidebar' import PostPage from '@/pages/PostPage' import PostNewPage from '@/pages/PostNewPage' import PostDetailPage from '@/pages/PostDetailPage' @@ -16,15 +17,13 @@ import { camelizeKeys } from 'humps' import type { Post, Tag, User } from '@/types' -const App = () => { - const [posts, setPosts] = useState ([]) +export default () => { const [user, setUser] = useState (null) useEffect (() => { const createUser = () => ( axios.post (`${ API_BASE_URL }/users`) .then (res => { - if (res.data.code) { localStorage.setItem ('user_code', res.data.code) @@ -56,28 +55,18 @@ const App = () => {
- - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + {/* } /> */} -
- - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* } /> */} - -
) } - - -export default App diff --git a/frontend/src/components/TagDetailSidebar.tsx b/frontend/src/components/TagDetailSidebar.tsx new file mode 100644 index 0000000..077f58c --- /dev/null +++ b/frontend/src/components/TagDetailSidebar.tsx @@ -0,0 +1,60 @@ +import React, { useEffect, useState } from 'react' +import axios from 'axios' +import { Link, useParams } from 'react-router-dom' +import { API_BASE_URL } from '@/config' +import TagSearch from './TagSearch' +import SidebarComponent from './layout/SidebarComponent' + +import type { Post, Tag } from '@/types' + +type TagByCategory = { [key: string]: Tag[] } + +type Props = { post: Post | null } + + +export default ({ post }: Props) => { + const [tags, setTags] = useState ({ }) + + const categoryNames: { [key: string]: string } = { + general: '一般', + deerjikist: 'ニジラー', + nico: 'ニコニコタグ' } + + useEffect (() => { + if (!(post)) + return + + const fetchTags = async () => { + const tagsTmp: TagByCategory = { } + for (const tag of post.tags) + { + if (!(tag.category in tagsTmp)) + tagsTmp[tag.category] = [] + tagsTmp[tag.category].push (tag) + } + for (const cat of Object.keys (tagsTmp)) + tagsTmp[cat].sort ((tagA, tagB) => tagA.name < tagB.name ? -1 : 1) + setTags (tagsTmp) + } + + fetchTags () + }, [post]) + + return ( + + + {['general', 'deerjikist', 'nico'].map (cat => cat in tags && ( + <> +

{categoryNames[cat]}

+
    + {tags[cat].map (tag => ( +
  • + + {tag.name} + +
  • ))} +
+ ))} +
) +} diff --git a/frontend/src/components/TagSidebar.tsx b/frontend/src/components/TagSidebar.tsx index 264d753..842bebb 100644 --- a/frontend/src/components/TagSidebar.tsx +++ b/frontend/src/components/TagSidebar.tsx @@ -1,15 +1,15 @@ import React, { useEffect, useState } from 'react' import axios from 'axios' import { Link, useParams } from 'react-router-dom' -import { API_BASE_URL } from '../config' +import { API_BASE_URL } from '@/config' import TagSearch from './TagSearch' +import SidebarComponent from './layout/SidebarComponent' import type { Post, Tag } from '@/types' type TagByCategory = { [key: string]: Tag[] } -type Props = { posts: Post[] - setPosts: (posts: Post[]) => void } +type Props = { posts: Post[] } const tagNameMap: { [key: string]: string } = { @@ -17,46 +17,35 @@ const tagNameMap: { [key: string]: string } = { deerjikist: 'ニジラー', nico: 'ニコニコタグ' } -const TagSidebar: React.FC = (props: Props) => { - const { posts, setPosts } = props +export default ({ posts }: Props) => { const [tags, setTags] = useState ({ }) - const [tagsCounts, setTagsCounts] = useState<{ [key: id]: number }> ({ }) + const [tagsCounts, setTagsCounts] = useState<{ [key: number]: number }> ({ }) useEffect (() => { - const fetchTags = async () => { - try + const tagsTmp: TagByCategory = { } + const tagsCountsTmp: { [key: number]: number } = { } + for (const post of posts) { - let tagsTmp: TagByCategory = { } - let tagsCountsTmp: { [key: id]: number } = { } - for (const post of posts) + for (const tag of post.tags) { - for (const tag of post.tags) - { - if (!(tag.category in tagsTmp)) - tagsTmp[tag.category] = [] - tagsTmp[tag.category].push (tag) - if (!(tag.id in tagsCountsTmp)) - tagsCountsTmp[tag.id] = 0 - ++tagsCountsTmp[tag.id] - } + if (!(tag.category in tagsTmp)) + tagsTmp[tag.category] = [] + if (!(tagsTmp[tag.category].map (t => t.id).includes (tag.id))) + tagsTmp[tag.category].push (tag) + if (!(tag.id in tagsCountsTmp)) + tagsCountsTmp[tag.id] = 0 + ++tagsCountsTmp[tag.id] } - for (const cat of Object.keys (tagsTmp)) - tagsTmp[cat].sort ((tagA, tagB) => tagA.name < tagB.name ? -1 : 1) - setTags (tagsTmp) - setTagsCounts (tagsCountsTmp) } - catch (error) - { - console.error ('Failed to fetch tags:', error) - } - } - - fetchTags () + for (const cat of Object.keys (tagsTmp)) + tagsTmp[cat].sort ((tagA, tagB) => tagA.name < tagB.name ? -1 : 1) + setTags (tagsTmp) + setTagsCounts (tagsCountsTmp) }, [posts]) return ( -
+ {['general', 'deerjikist', 'nico'].map (cat => cat in tags && <>

{tagNameMap[cat]}

@@ -67,11 +56,9 @@ const TagSidebar: React.FC = (props: Props) => { className="text-blue-600 hover:underline"> {tag.name} - {posts.length > 1 && {tagsCounts[tag.id]}} + {tagsCounts[tag.id]} ))} )} -
) + ) } - -export default TagSidebar diff --git a/frontend/src/components/layout/MainArea.tsx b/frontend/src/components/layout/MainArea.tsx new file mode 100644 index 0000000..c2117a3 --- /dev/null +++ b/frontend/src/components/layout/MainArea.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +type Props = { children: React.ReactNode } + + +export default ({ children }: Props) => ( +
+ {children} +
) diff --git a/frontend/src/components/layout/SidebarComponent.tsx b/frontend/src/components/layout/SidebarComponent.tsx new file mode 100644 index 0000000..f975612 --- /dev/null +++ b/frontend/src/components/layout/SidebarComponent.tsx @@ -0,0 +1,9 @@ +import React from 'react' + +type Props = { children: React.ReactNode } + + +export default ({ children }: Props) => ( +
+ {children} +
) diff --git a/frontend/src/pages/PostDetailPage.tsx b/frontend/src/pages/PostDetailPage.tsx index 87a35ef..394c7f0 100644 --- a/frontend/src/pages/PostDetailPage.tsx +++ b/frontend/src/pages/PostDetailPage.tsx @@ -1,32 +1,33 @@ import React, { useEffect, useState } from 'react' import { Link, useLocation, useParams } from 'react-router-dom' import axios from 'axios' -import { API_BASE_URL, SITE_TITLE } from '../config' -import NicoViewer from '../components/NicoViewer' +import { API_BASE_URL, SITE_TITLE } from '@/config' +import NicoViewer from '@/components/NicoViewer' import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/use-toast' import { cn } from '@/lib/utils' +import MainArea from '@/components/layout/MainArea' +import TagDetailSidebar from '@/components/TagDetailSidebar' import type { Post, Tag } from '@/types' -type Props = { posts: Post[] - setPosts: (posts: Post[]) => void } - -const PostDetailPage = ({ posts, setPosts }: Props) => { +const PostDetailPage = () => { const { id } = useParams () + const [post, setPost] = useState (null) + const location = useLocation () const changeViewedFlg = () => { - if (posts[0]?.viewed) + if (post?.viewed) { void (axios.delete ( `${ API_BASE_URL }/posts/${ id }/viewed`, { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } }) - .then (res => setPosts (([post]) => { + .then (res => setPost (post => { post.viewed = false - return [post] + return post })) .catch (err => toast ({ title: '失敗……', description: '通信に失敗しました……' }))) @@ -37,9 +38,9 @@ const PostDetailPage = ({ posts, setPosts }: Props) => { `${ API_BASE_URL }/posts/${ id }/viewed`, { }, { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } }) - .then (res => setPosts (([post]) => { + .then (res => setPost (post => { post.viewed = true - return [post] + return post })) .catch (err => toast ({ title: '失敗……', description: '通信に失敗しました……' }))) @@ -51,40 +52,51 @@ const PostDetailPage = ({ posts, setPosts }: Props) => { return void (axios.get (`${ API_BASE_URL }/posts/${ id }`, { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } }) - .then (res => setPosts ([res.data])) + .then (res => setPost (res.data)) .catch (err => console.error ('うんち!', err))) }, [id]) - if (!(posts.length)) - return
Loading...
- - const post = posts[0] + if (!(post)) + return ( + <> + + Loading... + ) - document.title = `${ post.title || post.url } | ${ SITE_TITLE }` + if (post) + document.title = `${ post.title || post.url } | ${ SITE_TITLE }` - const url = new URL (post.url) + const url = post ? new URL (post.url) : undefined return ( -
- {(() => { - if (url.hostname.split ('.').slice (-2).join ('.') === 'nicovideo.jp') - { - return ( - ) - } - else - return {post.url} - }) ()} - -
) + <> + + + {post + ? ( +
+ {(() => { + if (url.hostname.split ('.').slice (-2).join ('.') === 'nicovideo.jp') + { + return ( + ) + } + else + return {post.url} + }) ()} + +
) + : 'Loading...'} +
+ ) } diff --git a/frontend/src/pages/PostNewPage.tsx b/frontend/src/pages/PostNewPage.tsx index c61e366..cc228a7 100644 --- a/frontend/src/pages/PostNewPage.tsx +++ b/frontend/src/pages/PostNewPage.tsx @@ -6,6 +6,7 @@ import NicoViewer from '../components/NicoViewer' import { Button } from '@/components/ui/button' import { toast } from '@/components/ui/use-toast' import { cn } from '@/lib/utils' +import MainArea from '@/components/layout/MainArea' import type { Post, Tag } from '@/types' @@ -14,7 +15,7 @@ type Props = { posts: Post[] setPosts: (posts: Post[]) => void } -const PostNewPage = () => { +export default () => { const location = useLocation () const navigate = useNavigate () @@ -112,99 +113,98 @@ const PostNewPage = () => { } return ( -
-

広場に投稿を追加する

- - {/* URL */} -
- - setURL (e.target.value)} - className="w-full border p-2 rounded" - onBlur={handleURLBlur} /> -
+ +
+

広場に投稿を追加する

+ + {/* URL */} +
+ + setURL (e.target.value)} + className="w-full border p-2 rounded" + onBlur={handleURLBlur} /> +
- {/* タイトル */} -
-
- - + {/* タイトル */} +
+
+ + +
+ setTitle (e.target.value)} + disabled={titleAutoFlg} />
- setTitle (e.target.value)} - disabled={titleAutoFlg} /> -
- {/* サムネール */} -
-
- - + {/* サムネール */} +
+
+ + +
+ {thumbnailAutoFlg + ? (thumbnailLoading + ?

Loading...

+ : !(thumbnailPreview) && ( +

+ URL から自動取得されます。 +

)) + : ( + { + const file = e.target.files?.[0] + if (file) + { + setThumbnailFile (file) + setThumbnailPreview (URL.createObjectURL (file)) + } + }} />)} + {thumbnailPreview && ( + preview)}
- {thumbnailAutoFlg - ? (thumbnailLoading - ?

Loading...

- : !(thumbnailPreview) && ( -

- URL から自動取得されます。 -

)) - : ( - { - const file = e.target.files?.[0] - if (file) - { - setThumbnailFile (file) - setThumbnailPreview (URL.createObjectURL (file)) - } - }} />)} - {thumbnailPreview && ( - preview)} -
- {/* タグ */} -
- - -
+ {/* タグ */} +
+ + +
- {/* 送信 */} - -
) + {/* 送信 */} + +
+ ) } - - -export default PostNewPage diff --git a/frontend/src/pages/PostPage.tsx b/frontend/src/pages/PostPage.tsx index cb7d7c8..80859fc 100644 --- a/frontend/src/pages/PostPage.tsx +++ b/frontend/src/pages/PostPage.tsx @@ -2,15 +2,14 @@ import React, { useEffect, useState } from 'react' import { Link, useLocation } from 'react-router-dom' import axios from 'axios' import { API_BASE_URL, SITE_TITLE } from '@/config' +import TagSidebar from '@/components/TagSidebar' +import MainArea from '@/components/layout/MainArea' import type { Post, Tag } from '@/types' -type Props = { posts: Post[] - setPosts: (posts: Post[]) => void } - -const PostPage = (props: Props) => { - const { posts, setPosts } = props +export default () => { + const [posts, setPosts] = useState ([]) const location = useLocation () const query = new URLSearchParams (location.search) @@ -42,17 +41,19 @@ const PostPage = (props: Props) => { }, [location.search]) return ( -
- {posts.map (post => ( - - - - ))} -
) + <> + + +
+ {posts.map (post => ( + + + + ))} +
+
+ ) } - - -export default PostPage diff --git a/frontend/src/pages/TagPage.tsx b/frontend/src/pages/TagPage.tsx index 732983b..f4a1ebe 100644 --- a/frontend/src/pages/TagPage.tsx +++ b/frontend/src/pages/TagPage.tsx @@ -2,8 +2,10 @@ import React, { useEffect, useState } from 'react' import axios from 'axios' import { useParams } from 'react-router-dom' import { API_BASE_URL } from '../config' +import MainArea from '@/components/layout/MainArea' -const TagPage = () => { + +export default () => { const { id } = useParams() const [posts, setPosts] = useState([]) const [tagName, setTagName] = useState('') @@ -32,18 +34,17 @@ const TagPage = () => { }, [id]) return ( -
-

タグ: {tagName}

-
- {posts.map ((post, index) => ( -
- {post.title} -

{post.title}

+ +
+

タグ: {tagName}

+
+ {posts.map ((post, index) => ( +
+ {post.title} +

{post.title}

+
+ ))}
- ))} -
-
- ) +
+ ) } - -export default TagPage diff --git a/frontend/src/pages/WikiDetailPage.tsx b/frontend/src/pages/WikiDetailPage.tsx index d9a52b0..42a9c25 100644 --- a/frontend/src/pages/WikiDetailPage.tsx +++ b/frontend/src/pages/WikiDetailPage.tsx @@ -3,9 +3,10 @@ import { Link, useParams, useNavigate } from 'react-router-dom' import ReactMarkdown from 'react-markdown' import axios from 'axios' import { API_BASE_URL } from '@/config' +import MainArea from '@/components/layout/MainArea' -const WikiDetailPage = () => { +export default () => { const { name } = useParams () const navigate = useNavigate () @@ -26,15 +27,14 @@ const WikiDetailPage = () => { }, [name]) return ( -
- (href?.startsWith ('/') - ? {children} - : {children})) }}> - {markdown || `このページは存在しません。[新規作成してください](/wiki/new?title=${ name })。`} - -
) + +
+ (href?.startsWith ('/') + ? {children} + : {children})) }}> + {markdown || `このページは存在しません。[新規作成してください](/wiki/new?title=${ name })。`} + +
+
) } - - -export default WikiDetailPage diff --git a/frontend/src/pages/WikiNewPage.tsx b/frontend/src/pages/WikiNewPage.tsx index 2f46021..a32fec9 100644 --- a/frontend/src/pages/WikiNewPage.tsx +++ b/frontend/src/pages/WikiNewPage.tsx @@ -9,13 +9,14 @@ import { cn } from '@/lib/utils' import MarkdownIt from 'markdown-it' import MdEditor from 'react-markdown-editor-lite' import 'react-markdown-editor-lite/lib/index.css' +import MainArea from '@/components/layout/MainArea' import type { Tag } from '@/types' const mdParser = new MarkdownIt -const WikiNewPage = () => { +export default () => { const location = useLocation () const navigate = useNavigate () @@ -46,35 +47,34 @@ const WikiNewPage = () => { }, []) return ( -
-

新規 Wiki ページ

+ +
+

新規 Wiki ページ

- {/* タイトル */} - {/* TODO: タグ補完 */} -
- - setTitle (e.target.value)} - className="w-full border p-2 rounded" /> -
+ {/* タイトル */} + {/* TODO: タグ補完 */} +
+ + setTitle (e.target.value)} + className="w-full border p-2 rounded" /> +
- {/* 本文 */} -
- - mdParser.render (text)} - onChange={({ text }) => setBody (text)} /> -
+ {/* 本文 */} +
+ + mdParser.render (text)} + onChange={({ text }) => setBody (text)} /> +
- {/* 送信 */} - -
) + {/* 送信 */} + +
+ ) } - - -export default WikiNewPage