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