#49 ぼちぼち

This commit is contained in:
2025-06-28 02:32:40 +09:00
parent ba1c1f1adf
commit 8020586ec6
19 changed files with 168 additions and 106 deletions
+3 -3
View File
@@ -4,9 +4,9 @@ import TagPage from '@/pages/TagPage'
import TopNav from '@/components/TopNav' import TopNav from '@/components/TopNav'
import TagSidebar from '@/components/TagSidebar' import TagSidebar from '@/components/TagSidebar'
import TagDetailSidebar from '@/components/TagDetailSidebar' import TagDetailSidebar from '@/components/TagDetailSidebar'
import PostPage from '@/pages/PostPage' import PostPage from '@/pages/posts/PostPage'
import PostNewPage from '@/pages/PostNewPage' import PostNewPage from '@/pages/posts/PostNewPage'
import PostDetailPage from '@/pages/PostDetailPage' import PostDetailPage from '@/pages/posts/PostDetailPage'
import WikiPage from '@/pages/WikiPage' import WikiPage from '@/pages/WikiPage'
import WikiNewPage from '@/pages/WikiNewPage' import WikiNewPage from '@/pages/WikiNewPage'
import WikiEditPage from '@/pages/WikiEditPage' import WikiEditPage from '@/pages/WikiEditPage'
+3 -2
View File
@@ -1,6 +1,7 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import axios from 'axios' import axios from 'axios'
import { API_BASE_URL } from '@/config' import { API_BASE_URL } from '@/config'
import { Button } from '@/components/ui/button'
import type { Post, Tag } from '@/types' import type { Post, Tag } from '@/types'
@@ -64,9 +65,9 @@ export default ({ post, onSave }: Props) => {
</div> </div>
{/* 送信 */} {/* 送信 */}
<button onClick={handleSubmit} <Button onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"> className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400">
</button> </Button>
</div>) </div>)
} }
+10 -9
View File
@@ -1,9 +1,11 @@
import React, { useEffect, useState } from 'react'
import axios from 'axios' import axios from 'axios'
import React, { useEffect, useState } from 'react'
import { Link, useParams } from 'react-router-dom' import { Link, useParams } from 'react-router-dom'
import TagSearch from '@/components/TagSearch'
import SubsectionTitle from '@/components/common/SubsectionTitle'
import SidebarComponent from '@/components/layout/SidebarComponent'
import { API_BASE_URL } from '@/config' import { API_BASE_URL } from '@/config'
import TagSearch from './TagSearch'
import SidebarComponent from './layout/SidebarComponent'
import { CATEGORIES } from '@/consts' import { CATEGORIES } from '@/consts'
import type { Category, Post, Tag } from '@/types' import type { Category, Post, Tag } from '@/types'
@@ -45,17 +47,16 @@ export default ({ post }: Props) => {
<SidebarComponent> <SidebarComponent>
<TagSearch /> <TagSearch />
{CATEGORIES.map ((cat: Category) => cat in tags && ( {CATEGORIES.map ((cat: Category) => cat in tags && (
<> <div className="my-3">
<h2>{categoryNames[cat]}</h2> <SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
<ul> <ul>
{tags[cat].map (tag => ( {tags[cat].map (tag => (
<li key={tag.id} className="mb-2"> <li key={tag.id} className="mb-1">
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`} <Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}>
className="text-blue-600 hover:underline">
{tag.name} {tag.name}
</Link> </Link>
</li>))} </li>))}
</ul> </ul>
</>))} </div>))}
</SidebarComponent>) </SidebarComponent>)
} }
+6 -3
View File
@@ -1,7 +1,8 @@
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import axios from 'axios' import axios from 'axios'
import { Link, useNavigate, useLocation } from 'react-router-dom' import { Link, useNavigate, useLocation } from 'react-router-dom'
import { API_BASE_URL } from '../config' import { API_BASE_URL } from '@/config'
import { cn } from '@/lib/utils'
import type { Tag } from '@/types' import type { Tag } from '@/types'
@@ -23,8 +24,10 @@ const TagSearchBox: React.FC = (props: Props) => {
<ul className="absolute left-0 right-0 z-50 w-full bg-gray-800 border border-gray-600 rounded shadow"> <ul className="absolute left-0 right-0 z-50 w-full bg-gray-800 border border-gray-600 rounded shadow">
{suggestions.map ((tag, i) => ( {suggestions.map ((tag, i) => (
<li key={tag.id} <li key={tag.id}
className={`px-3 py-2 cursor-pointer ${ className={cn ('px-3 py-2 cursor-pointer',
i === activeIndex ? 'bg-blue-600 text-white' : 'hover:bg-gray-700' }`} (i === activeIndex
? 'bg-blue-600 text-white'
: 'hover:bg-gray-700'))}
onMouseDown={() => onSelect (tag)} onMouseDown={() => onSelect (tag)}
> >
{tag.name} {tag.name}
+11 -14
View File
@@ -2,8 +2,9 @@ import React, { useEffect, useState } from 'react'
import axios from 'axios' import axios from 'axios'
import { Link, useParams } from 'react-router-dom' import { Link, useParams } from 'react-router-dom'
import { API_BASE_URL } from '@/config' import { API_BASE_URL } from '@/config'
import TagSearch from './TagSearch' import TagSearch from '@/components/TagSearch'
import SidebarComponent from './layout/SidebarComponent' import SidebarComponent from '@/components/layout/SidebarComponent'
import SectionTitle from '@/components/common/SectionTitle'
import type { Post, Tag } from '@/types' import type { Post, Tag } from '@/types'
@@ -12,12 +13,6 @@ type TagByCategory = { [key: string]: Tag[] }
type Props = { posts: Post[] } type Props = { posts: Post[] }
const tagNameMap: { [key: string]: string } = {
general: '一般',
deerjikist: 'ニジラー',
nico: 'ニコニコタグ' }
export default ({ posts }: Props) => { export default ({ posts }: Props) => {
const [tags, setTags] = useState<TagByCategory> ({ }) const [tags, setTags] = useState<TagByCategory> ({ })
const [tagsCounts, setTagsCounts] = useState<{ [key: number]: number }> ({ }) const [tagsCounts, setTagsCounts] = useState<{ [key: number]: number }> ({ })
@@ -47,18 +42,20 @@ export default ({ posts }: Props) => {
return ( return (
<SidebarComponent> <SidebarComponent>
<TagSearch /> <TagSearch />
{['general', 'deerjikist', 'nico'].map (cat => cat in tags && <> <SectionTitle></SectionTitle>
<h2>{tagNameMap[cat]}</h2>
<ul> <ul>
{['general', 'deerjikist', 'nico'].map (cat => cat in tags && (
<>
{tags[cat].map (tag => ( {tags[cat].map (tag => (
<li key={tag.id} className="mb-2"> <li key={tag.id} className="mb-1">
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`} <Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}>
className="text-blue-600 hover:underline">
{tag.name} {tag.name}
</Link> </Link>
<span className="ml-1">{tagsCounts[tag.id]}</span> <span className="ml-1">{tagsCounts[tag.id]}</span>
</li>))} </li>))}
</>))}
</ul> </ul>
</>)} <SectionTitle></SectionTitle>
<Link></Link>
</SidebarComponent>) </SidebarComponent>)
} }
+2 -2
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
import { Link, useLocation, useNavigate, useParams } from 'react-router-dom' import { Link, useLocation, useNavigate, useParams } from 'react-router-dom'
import SettingsDialogue from './SettingsDialogue' import SettingsDialogue from './SettingsDialogue'
import { Button } from './ui/button' import { Button } from './ui/button'
import clsx from 'clsx' import { cn } from '@/lib/utils'
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
import type { User } from '@/types' import type { User } from '@/types'
@@ -33,7 +33,7 @@ const TopNav: React.FC = ({ user, setUser }: Props) => {
title: string title: string
menu?: Menu menu?: Menu
base?: string }) => ( base?: string }) => (
<Link to={to} className={clsx ('hover:text-orange-500 h-full flex items-center', <Link to={to} className={cn ('hover:text-orange-500 h-full flex items-center',
(location.pathname.startsWith (base ?? to) (location.pathname.startsWith (base ?? to)
? 'bg-gray-700 px-4 font-bold' ? 'bg-gray-700 px-4 font-bold'
: 'px-2'))}> : 'px-2'))}>
+9
View File
@@ -0,0 +1,9 @@
import React from 'react'
type Props = { children: React.ReactNode }
export default ({ children }: Props) => (
<div className="max-w-xl mx-auto p-4 space-y-4">
{children}
</div>)
+28
View File
@@ -0,0 +1,28 @@
import React from 'react'
type Props = { children: React.ReactNode
checkBox?: { label: string
checked: boolean
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void } }
export default ({ children, checkBox }: Props) => {
if (!(checkBox))
{
return (
<label className="block font-semibold mb-1">
{children}
</label>)
}
return (
<div className="flex gap-2 mb-1">
<label className="flex-1 block font-semibold">{children}</label>
<label className="flex items-center block gap-1">
<input type="checkbox"
checked={checkBox.checked}
onChange={checkBox.onChange} />
{checkBox.label}
</label>
</div>)
}
@@ -0,0 +1,9 @@
import React from 'react'
type Props = { children: React.ReactNode }
export default ({ children }: Props) => (
<h1 className="text-2xl font-bold mb-2">
{children}
</h1>)
@@ -0,0 +1,9 @@
import React from 'react'
type Props = { children: React.ReactNode }
export default ({ children }: Props) => (
<h2 className="text-xl my-4">
{children}
</h2>)
@@ -0,0 +1,9 @@
import React from 'react'
type Props = { children: React.ReactNode }
export default ({ children }: Props) => (
<h3 className="my-2">
{children}
</h3>)
+3 -2
View File
@@ -1,4 +1,5 @@
import React, { useState } from 'react' import React, { useState } from 'react'
import { cn } from '@/lib/utils'
type TabProps = { name: string type TabProps = { name: string
init?: boolean init?: boolean
@@ -6,7 +7,7 @@ type TabProps = { name: string
type Props = { children: React.ReactElement<{ name: string }>[] } type Props = { children: React.ReactElement<{ name: string }>[] }
export const Tab = ({ children }: TabProps) => children export const Tab = ({ children }: TabProps) => <>{children}</>
export default ({ children }: Props) => { export default ({ children }: Props) => {
@@ -23,7 +24,7 @@ export default ({ children }: Props) => {
{tabs.map ((tab, i) => ( {tabs.map ((tab, i) => (
<a key={i} <a key={i}
href="#" href="#"
className={`text-blue-400 hover:underline ${ i === current ? 'font-bold' : '' }`} className={cn (i === current && 'font-bold')}
onClick={ev => { onClick={ev => {
ev.preventDefault () ev.preventDefault ()
setCurrent (i) setCurrent (i)
+8 -9
View File
@@ -6,6 +6,7 @@ import axios from 'axios'
import { API_BASE_URL, SITE_TITLE } from '@/config' import { API_BASE_URL, SITE_TITLE } from '@/config'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
import PageTitle from '@/components/common/PageTitle'
import type { WikiPage } from '@/types' import type { WikiPage } from '@/types'
@@ -45,20 +46,18 @@ export default () => {
{(wikiPage && version) && ( {(wikiPage && version) && (
<div className="text-sm flex gap-3 items-center justify-center border border-gray-700 rounded px-2 py-1 mb-4"> <div className="text-sm flex gap-3 items-center justify-center border border-gray-700 rounded px-2 py-1 mb-4">
{wikiPage.pred ? ( {wikiPage.pred ? (
<Link to={`/wiki/${ title }?version=${ wikiPage.pred }`} <Link to={`/wiki/${ title }?version=${ wikiPage.pred }`}>
className="text-blue-400 hover:underline"> &lt;
&lt; </Link>) : <>()</>}
</Link>) : <>&lt; </>}
<span>{wikiPage.updated_at}</span> <span>{wikiPage.updated_at}</span>
{wikiPage.succ ? ( {wikiPage.succ ? (
<Link to={`/wiki/${ title }?version=${ wikiPage.succ }`} <Link to={`/wiki/${ title }?version=${ wikiPage.succ }`}>
className="text-blue-400 hover:underline"> &gt;
&gt; </Link>) : <>()</>}
</Link>) : <> &gt;</>}
</div>)} </div>)}
<h1>{title}</h1> <PageTitle>{title}</PageTitle>
<div className="prose mx-auto p-4"> <div className="prose mx-auto p-4">
{wikiPage === undefined ? 'Loading...' : ( {wikiPage === undefined ? 'Loading...' : (
<> <>
+4 -2
View File
@@ -1,7 +1,9 @@
import axios from 'axios'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { Link, useLocation, useParams } from 'react-router-dom' import { Link, useLocation, useParams } from 'react-router-dom'
import axios from 'axios'
import PageTitle from '@/components/common/PageTitle'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { API_BASE_URL, SITE_TITLE } from '@/config' import { API_BASE_URL, SITE_TITLE } from '@/config'
@@ -29,7 +31,7 @@ export default () => {
<Helmet> <Helmet>
<title>{`Wiki 差分: ${ diff?.title } | ${ SITE_TITLE }`}</title> <title>{`Wiki 差分: ${ diff?.title } | ${ SITE_TITLE }`}</title>
</Helmet> </Helmet>
<h1>{diff?.title}</h1> <PageTitle>{diff?.title}</PageTitle>
<div className="prose mx-auto p-4"> <div className="prose mx-auto p-4">
{diff ? ( {diff ? (
diff.diff.map (d => ( diff.diff.map (d => (
+2 -4
View File
@@ -44,8 +44,7 @@ export default () => {
</Link>)} </Link>)}
</td> </td>
<td className="p-2"> <td className="p-2">
<Link to={`/wiki/${ encodeURIComponent (change.wiki_page.title) }?version=${ change.sha }`} <Link to={`/wiki/${ encodeURIComponent (change.wiki_page.title) }?version=${ change.sha }`}>
className="text-blue-400 hover:underline">
{change.wiki_page.title} {change.wiki_page.title}
</Link> </Link>
</td> </td>
@@ -63,8 +62,7 @@ export default () => {
}) ()} }) ()}
</td> </td>
<td className="p-2"> <td className="p-2">
<Link to={`/users/${ change.user.id }`} <Link to={`/users/${ change.user.id }`}>
className="text-blue-400 hover:underline">
{change.user.name} {change.user.name}
</Link> </Link>
<br /> <br />
+3 -3
View File
@@ -4,6 +4,7 @@ import { Link } from 'react-router-dom'
import axios from 'axios' import axios from 'axios'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import { API_BASE_URL, SITE_TITLE } from '@/config' import { API_BASE_URL, SITE_TITLE } from '@/config'
import SectionTitle from '@/components/common/SectionTitle'
import type { Category, WikiPage } from '@/types' import type { Category, WikiPage } from '@/types'
@@ -34,7 +35,7 @@ export default () => {
<title>{`Wiki | ${ SITE_TITLE }`}</title> <title>{`Wiki | ${ SITE_TITLE }`}</title>
</Helmet> </Helmet>
<div className="max-w-xl"> <div className="max-w-xl">
<h2 className="text-xl mb-4">Wiki</h2> <SectionTitle className="text-xl mb-4">Wiki</SectionTitle>
<form onSubmit={handleSearch} className="space-y-2"> <form onSubmit={handleSearch} className="space-y-2">
{/* タイトル */} {/* タイトル */}
<div> <div>
@@ -76,8 +77,7 @@ export default () => {
{results.map (page => ( {results.map (page => (
<tr key={page.id}> <tr key={page.id}>
<td className="p-2"> <td className="p-2">
<Link to={`/wiki/${ encodeURIComponent (page.title) }`} <Link to={`/wiki/${ encodeURIComponent (page.title) }`}>
className="text-blue-400 hover:underline">
{page.title} {page.title}
</Link> </Link>
</td> </td>
@@ -65,7 +65,14 @@ export default ({ user }: Props) => {
setEditing (true) setEditing (true)
}, [editing]) }, [editing])
const url = post ? new URL (post.url) : undefined const url = post ? new URL (post.url) : null
const nicoFlg = url?.hostname.split ('.').slice (-2).join ('.') === 'nicovideo.jp'
const videoId = (nicoFlg
? url.pathname.match (/(?<=\/watch\/)[a-zA-Z0-9]+?(?=\/|$)/)[0]
: '')
const viewedClass = (post?.viewed
? 'bg-blue-600 hover:bg-blue-700'
: 'bg-gray-500 hover:bg-gray-600')
return ( return (
<> <>
@@ -76,23 +83,15 @@ export default ({ user }: Props) => {
<MainArea> <MainArea>
{post {post
? ( ? (
<div className="p-4"> <>
{(() => { {nicoFlg
if (url.hostname.split ('.').slice (-2).join ('.') === 'nicovideo.jp') ? (
{ <NicoViewer id={videoId}
return (
<NicoViewer
id={url.pathname.match (
/(?<=\/watch\/)[a-zA-Z0-9]+?(?=\/|$)/)[0]}
width="640" width="640"
height="360" />) height="360" />)
} : <img src={post.thumbnail} alt={post.url} className="mb-4 w-full" />}
else
return <img src={post.thumbnail} alt={post.url} className="mb-4 w-full" />
}) ()}
<Button onClick={changeViewedFlg} <Button onClick={changeViewedFlg}
className={cn ('text-white', className={cn ('text-white', viewedClass)}>
post.viewed ? 'bg-blue-600 hover:bg-blue-700' : 'bg-gray-500 hover:bg-gray-600')}>
{post.viewed ? '閲覧済' : '未閲覧'} {post.viewed ? '閲覧済' : '未閲覧'}
</Button> </Button>
<TabGroup> <TabGroup>
@@ -105,7 +104,7 @@ export default ({ user }: Props) => {
}} /> }} />
</Tab>} </Tab>}
</TabGroup> </TabGroup>
</div>) </>)
: 'Loading...'} : 'Loading...'}
</MainArea> </MainArea>
</>) </>)
@@ -8,6 +8,9 @@ import { Button } from '@/components/ui/button'
import { toast } from '@/components/ui/use-toast' import { toast } from '@/components/ui/use-toast'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import MainArea from '@/components/layout/MainArea' import MainArea from '@/components/layout/MainArea'
import Form from '@/components/common/Form'
import PageTitle from '@/components/common/PageTitle'
import Label from '@/components/common/Label'
import type { Post, Tag } from '@/types' import type { Post, Tag } from '@/types'
@@ -115,12 +118,12 @@ export default () => {
<Helmet> <Helmet>
<title>{`広場に投稿を追加 | ${ SITE_TITLE }`}</title> <title>{`広場に投稿を追加 | ${ SITE_TITLE }`}</title>
</Helmet> </Helmet>
<div className="max-w-xl mx-auto p-4 space-y-4"> <Form>
<h1 className="text-2xl font-bold mb-2">稿</h1> <PageTitle>稿</PageTitle>
{/* URL */} {/* URL */}
<div> <div>
<label className="block font-semibold mb-1">URL</label> <Label>URL</Label>
<input type="text" <input type="text"
placeholder="例:https://www.nicovideo.jp/watch/..." placeholder="例:https://www.nicovideo.jp/watch/..."
value={url} value={url}
@@ -131,15 +134,12 @@ export default () => {
{/* タイトル */} {/* タイトル */}
<div> <div>
<div className="flex gap-2 mb-1"> <Label checkBox={{
<label className="flex-1 block font-semibold"></label> label: '自動',
<label className="flex items-center block gap-1"> checked: titleAutoFlg,
<input type="checkbox" onChange: ev => setTitleAutoFlg (ev.target.checked)}}>
checked={titleAutoFlg}
onChange={e => setTitleAutoFlg (e.target.checked)} /> </Label>
</label>
</div>
<input type="text" <input type="text"
className="w-full border rounded p-2" className="w-full border rounded p-2"
value={title} value={title}
@@ -150,15 +150,12 @@ export default () => {
{/* サムネール */} {/* サムネール */}
<div> <div>
<div className="flex gap-2 mb-1"> <Label checkBox={{
<label className="block font-semibold flex-1"></label> label: '自動',
<label className="flex items-center gap-1"> checked: thumbnailAutoFlg,
<input type="checkbox" onChange: ev => setThumbnailAutoFlg (ev.target.checked)}}>
checked={thumbnailAutoFlg}
onChange={e => setThumbnailAutoFlg (e.target.checked)} /> </Label>
</label>
</div>
{thumbnailAutoFlg {thumbnailAutoFlg
? (thumbnailLoading ? (thumbnailLoading
? <p className="text-gray-500 text-sm">Loading...</p> ? <p className="text-gray-500 text-sm">Loading...</p>
@@ -185,7 +182,7 @@ export default () => {
{/* タグ */} {/* タグ */}
<div> <div>
<label className="block font-semibold"></label> <Label></Label>
<select multiple <select multiple
value={tagIds.map (String)} value={tagIds.map (String)}
onChange={e => { onChange={e => {
@@ -201,11 +198,11 @@ export default () => {
</div> </div>
{/* 送信 */} {/* 送信 */}
<button onClick={handleSubmit} <Button onClick={handleSubmit}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400" className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"
disabled={titleLoading || thumbnailLoading}> disabled={titleLoading || thumbnailLoading}>
</button> </Button>
</div> </Form>
</MainArea>) </MainArea>)
} }