コミットを比較
5 コミット
7d2cd85754
...
4ec9c9c5e0
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| 4ec9c9c5e0 | |||
| 5a4919116d | |||
| 69d111a133 | |||
| b3405a2d8d | |||
| 3f42eb6915 |
@@ -1,42 +0,0 @@
|
|||||||
#root {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo {
|
|
||||||
height: 6em;
|
|
||||||
padding: 1.5em;
|
|
||||||
will-change: filter;
|
|
||||||
transition: filter 300ms;
|
|
||||||
}
|
|
||||||
.logo:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #646cffaa);
|
|
||||||
}
|
|
||||||
.logo.react:hover {
|
|
||||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes logo-spin {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: no-preference) {
|
|
||||||
a:nth-of-type(2) .logo {
|
|
||||||
animation: logo-spin infinite 20s linear;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
padding: 2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.read-the-docs {
|
|
||||||
color: #888;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
|
|
||||||
|
import TagLink from '@/components/TagLink'
|
||||||
import TagSearch from '@/components/TagSearch'
|
import TagSearch from '@/components/TagSearch'
|
||||||
import SubsectionTitle from '@/components/common/SubsectionTitle'
|
import SubsectionTitle from '@/components/common/SubsectionTitle'
|
||||||
import SidebarComponent from '@/components/layout/SidebarComponent'
|
import SidebarComponent from '@/components/layout/SidebarComponent'
|
||||||
@@ -14,31 +14,34 @@ type Props = { post: Post | null }
|
|||||||
|
|
||||||
|
|
||||||
export default ({ post }: Props) => {
|
export default ({ post }: Props) => {
|
||||||
const [tags, setTags] = useState({ } as TagByCategory)
|
const [tags, setTags] = useState ({ } as TagByCategory)
|
||||||
|
|
||||||
const categoryNames: Partial<{ [key in Category]: string }> = {
|
const categoryNames: Record<Category, string> = {
|
||||||
general: '一般',
|
|
||||||
deerjikist: 'ニジラー',
|
deerjikist: 'ニジラー',
|
||||||
|
meme: '原作・ネタ元・ミーム等',
|
||||||
|
character: 'キャラクター',
|
||||||
|
general: '一般',
|
||||||
|
material: '素材',
|
||||||
|
meta: 'メタタグ',
|
||||||
nico: 'ニコニコタグ' }
|
nico: 'ニコニコタグ' }
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
if (!(post))
|
if (!(post))
|
||||||
return
|
return
|
||||||
|
|
||||||
const fetchTags = () => {
|
|
||||||
const tagsTmp = { } as TagByCategory
|
const tagsTmp = { } as TagByCategory
|
||||||
|
|
||||||
for (const tag of post.tags)
|
for (const tag of post.tags)
|
||||||
{
|
{
|
||||||
if (!(tag.category in tagsTmp))
|
if (!(tag.category in tagsTmp))
|
||||||
tagsTmp[tag.category] = []
|
tagsTmp[tag.category] = []
|
||||||
tagsTmp[tag.category].push (tag)
|
tagsTmp[tag.category].push (tag)
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[])
|
for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[])
|
||||||
tagsTmp[cat].sort ((tagA: Tag, tagB: Tag) => tagA.name < tagB.name ? -1 : 1)
|
tagsTmp[cat].sort ((tagA: Tag, tagB: Tag) => tagA.name < tagB.name ? -1 : 1)
|
||||||
setTags (tagsTmp)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchTags ()
|
setTags (tagsTmp)
|
||||||
}, [post])
|
}, [post])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -48,11 +51,9 @@ export default ({ post }: Props) => {
|
|||||||
<div className="my-3" key={cat}>
|
<div className="my-3" key={cat}>
|
||||||
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
|
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
|
||||||
<ul>
|
<ul>
|
||||||
{tags[cat].map (tag => (
|
{tags[cat].map ((tag, i) => (
|
||||||
<li key={tag.id} className="mb-1">
|
<li key={i} className="mb-1">
|
||||||
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}>
|
<TagLink tag={tag} />
|
||||||
{tag.name}
|
|
||||||
</Link>
|
|
||||||
</li>))}
|
</li>))}
|
||||||
</ul>
|
</ul>
|
||||||
</div>))}
|
</div>))}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
import type { Tag } from '@/types'
|
||||||
|
|
||||||
|
type Props = { tag: Tag
|
||||||
|
linkFlg?: boolean
|
||||||
|
withWiki?: boolean
|
||||||
|
withCount?: boolean }
|
||||||
|
|
||||||
|
|
||||||
|
export default ({ tag,
|
||||||
|
linkFlg = true,
|
||||||
|
withWiki = true,
|
||||||
|
withCount = true }: Props) => {
|
||||||
|
const spanClass = cn (
|
||||||
|
`text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE }`,
|
||||||
|
`dark:text-${ TAG_COLOUR[tag.category] }-${ DARK_COLOUR_SHADE }`)
|
||||||
|
const linkClass = cn (
|
||||||
|
spanClass,
|
||||||
|
`hover:text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE - 200 }`,
|
||||||
|
`dark:hover:text-${ TAG_COLOUR[tag.category] }-${ DARK_COLOUR_SHADE - 200 }`)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{(linkFlg && withWiki) && (
|
||||||
|
<span className="mr-1">
|
||||||
|
<Link to={`/wiki/${ encodeURIComponent (tag.name) }`}
|
||||||
|
className={linkClass}>
|
||||||
|
?
|
||||||
|
</Link>
|
||||||
|
</span>)}
|
||||||
|
{linkFlg
|
||||||
|
? (
|
||||||
|
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
|
||||||
|
className={linkClass}>
|
||||||
|
{tag.name}
|
||||||
|
</Link>)
|
||||||
|
: (
|
||||||
|
<span className={spanClass}>
|
||||||
|
{tag.name}
|
||||||
|
</span>)}
|
||||||
|
{withCount && (
|
||||||
|
<span className="ml-1">{tag.postCount}</span>)}
|
||||||
|
</>)
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
import { useLocation, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
|
import TagLink from '@/components/TagLink'
|
||||||
import TagSearch from '@/components/TagSearch'
|
import TagSearch from '@/components/TagSearch'
|
||||||
import SectionTitle from '@/components/common/SectionTitle'
|
import SectionTitle from '@/components/common/SectionTitle'
|
||||||
import SidebarComponent from '@/components/layout/SidebarComponent'
|
import SidebarComponent from '@/components/layout/SidebarComponent'
|
||||||
@@ -28,6 +29,7 @@ export default ({ posts }: Props) => {
|
|||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
const tagsTmp: TagByCategory = { }
|
const tagsTmp: TagByCategory = { }
|
||||||
let cnt = 0
|
let cnt = 0
|
||||||
|
|
||||||
loop:
|
loop:
|
||||||
for (const post of posts)
|
for (const post of posts)
|
||||||
{
|
{
|
||||||
@@ -35,6 +37,7 @@ export default ({ posts }: Props) => {
|
|||||||
{
|
{
|
||||||
if (!(tag.category in tagsTmp))
|
if (!(tag.category in tagsTmp))
|
||||||
tagsTmp[tag.category] = []
|
tagsTmp[tag.category] = []
|
||||||
|
|
||||||
if (!(tagsTmp[tag.category].map (t => t.id).includes (tag.id)))
|
if (!(tagsTmp[tag.category].map (t => t.id).includes (tag.id)))
|
||||||
{
|
{
|
||||||
tagsTmp[tag.category].push (tag)
|
tagsTmp[tag.category].push (tag)
|
||||||
@@ -44,8 +47,10 @@ export default ({ posts }: Props) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const cat of Object.keys (tagsTmp))
|
for (const cat of Object.keys (tagsTmp))
|
||||||
tagsTmp[cat].sort ((tagA, tagB) => tagA.name < tagB.name ? -1 : 1)
|
tagsTmp[cat].sort ((tagA, tagB) => tagA.name < tagB.name ? -1 : 1)
|
||||||
|
|
||||||
setTags (tagsTmp)
|
setTags (tagsTmp)
|
||||||
}, [posts])
|
}, [posts])
|
||||||
|
|
||||||
@@ -57,10 +62,7 @@ export default ({ posts }: Props) => {
|
|||||||
{CATEGORIES.flatMap (cat => cat in tags ? (
|
{CATEGORIES.flatMap (cat => cat in tags ? (
|
||||||
tags[cat].map (tag => (
|
tags[cat].map (tag => (
|
||||||
<li key={tag.id} className="mb-1">
|
<li key={tag.id} className="mb-1">
|
||||||
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}>
|
<TagLink tag={tag} />
|
||||||
{tag.name}
|
|
||||||
</Link>
|
|
||||||
<span className="ml-1">{tag.postCount}</span>
|
|
||||||
</li>))) : [])}
|
</li>))) : [])}
|
||||||
</ul>
|
</ul>
|
||||||
<SectionTitle>関聯</SectionTitle>
|
<SectionTitle>関聯</SectionTitle>
|
||||||
|
|||||||
@@ -1,49 +1,59 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import toCamel from 'camelcase-keys'
|
import toCamel from 'camelcase-keys'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
import { Link, useLocation, /* useNavigate */ } from 'react-router-dom'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { API_BASE_URL } from '@/config'
|
import { API_BASE_URL } from '@/config'
|
||||||
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
import { WikiIdBus } from '@/lib/eventBus/WikiIdBus'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import type { Tag, User, WikiPage } from '@/types'
|
import type { Menu, Tag, User, WikiPage } from '@/types'
|
||||||
|
|
||||||
type Props = { user: User | null }
|
type Props = { user: User | null }
|
||||||
|
|
||||||
const Menu = { None: 'None',
|
|
||||||
Post: 'Post',
|
|
||||||
User: 'User',
|
|
||||||
Tag: 'Tag',
|
|
||||||
Wiki: 'Wiki' } as const
|
|
||||||
|
|
||||||
type Menu = typeof Menu[keyof typeof Menu]
|
|
||||||
|
|
||||||
|
|
||||||
export default ({ user }: Props) => {
|
export default ({ user }: Props) => {
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
const navigate = useNavigate ()
|
// const navigate = useNavigate ()
|
||||||
|
|
||||||
const [selectedMenu, setSelectedMenu] = useState<Menu> (Menu.None)
|
|
||||||
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
|
|
||||||
// const [wikiSearch, setWikiSearch] = useState ('')
|
|
||||||
// const [activeIndex, setActiveIndex] = useState (-1)
|
// const [activeIndex, setActiveIndex] = useState (-1)
|
||||||
// const [suggestions, setSuggestions] = useState<WikiPage[]> ([])
|
const [postCount, setPostCount] = useState<number | null> (null)
|
||||||
// const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
|
// const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
|
||||||
|
// const [suggestions, setSuggestions] = useState<WikiPage[]> ([])
|
||||||
// const [tagSearch, setTagSearch] = useState ('')
|
// const [tagSearch, setTagSearch] = useState ('')
|
||||||
// const [userSearch, setUserSearch] = useState ('')
|
// const [userSearch, setUserSearch] = useState ('')
|
||||||
const [postCount, setPostCount] = useState<number | null> (null)
|
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
|
||||||
|
// const [wikiSearch, setWikiSearch] = useState ('')
|
||||||
|
|
||||||
const MyLink = ({ to, title, base }: { to: string
|
const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId)
|
||||||
title: string
|
const wikiTitle = location.pathname.split ('/')[2]
|
||||||
base?: string }) => (
|
const menu: Menu = [
|
||||||
<Link to={to} className={cn ('hover:text-orange-500 h-full flex items-center',
|
{ name: '広場', to: '/posts', subMenu: [
|
||||||
(location.pathname.startsWith (base ?? to)
|
{ name: '一覧', to: '/posts' },
|
||||||
? 'bg-gray-700 px-4 font-bold'
|
{ name: '投稿追加', to: '/posts/new' },
|
||||||
: 'px-2'))}>
|
{ name: 'ヘルプ', to: '/wiki/ヘルプ:広場' }] },
|
||||||
{title}
|
{ name: 'タグ', to: '/tags', subMenu: [
|
||||||
</Link>)
|
{ name: 'タグ一覧', to: '/tags', visible: false },
|
||||||
|
{ name: '別名タグ', to: '/tags/aliases', visible: false },
|
||||||
|
{ name: '上位タグ', to: '/tags/implications', visible: false },
|
||||||
|
{ name: 'ニコニコ連携', to: '/tags/posts' },
|
||||||
|
{ name: 'タグのつけ方', to: '/wiki/ヘルプ:タグのつけ方' },
|
||||||
|
{ name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' }] },
|
||||||
|
{ name: 'Wiki', to: '/wiki/ヘルプ:ホーム', base: '/wiki', subMenu: [
|
||||||
|
{ name: '検索', to: '/wiki' },
|
||||||
|
{ name: '新規', to: '/wiki/new' },
|
||||||
|
{ name: '全体履歴', to: '/wiki/changes' },
|
||||||
|
{ name: 'ヘルプ', to: '/wiki/ヘルプ:Wiki' },
|
||||||
|
{ component: <span className="flex items-center px-2">|</span>,
|
||||||
|
visible: wikiPageFlg },
|
||||||
|
{ name: `広場 (${ postCount || 0 })`, to: `/posts?tags=${ wikiTitle }`,
|
||||||
|
visible: wikiPageFlg },
|
||||||
|
{ name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
|
||||||
|
{ name: '編輯', to: `/wiki/${ wikiId || wikiTitle }/edit`, visible: wikiPageFlg }] },
|
||||||
|
{ name: 'ユーザ', to: '/users', subMenu: [
|
||||||
|
{ name: '一覧', to: '/users', visible: false },
|
||||||
|
{ name: 'お前', to: `/users/${ user?.id }`, visible: false },
|
||||||
|
{ name: '設定', to: '/users/settings', visible: Boolean (user) }] }]
|
||||||
|
|
||||||
// const whenTagSearchChanged = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
// const whenTagSearchChanged = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
// // TODO: 実装
|
// // TODO: 実装
|
||||||
@@ -100,33 +110,18 @@ export default ({ user }: Props) => {
|
|||||||
return () => unsubscribe ()
|
return () => unsubscribe ()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect (() => {
|
|
||||||
if (location.pathname.startsWith ('/posts'))
|
|
||||||
setSelectedMenu (Menu.Post)
|
|
||||||
else if (location.pathname.startsWith ('/users'))
|
|
||||||
setSelectedMenu (Menu.User)
|
|
||||||
else if (location.pathname.startsWith ('/tags'))
|
|
||||||
setSelectedMenu (Menu.Tag)
|
|
||||||
else if (location.pathname.startsWith ('/wiki'))
|
|
||||||
setSelectedMenu (Menu.Wiki)
|
|
||||||
else
|
|
||||||
setSelectedMenu (Menu.None)
|
|
||||||
}, [location])
|
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
if (!(wikiId))
|
if (!(wikiId))
|
||||||
return
|
return
|
||||||
|
|
||||||
void (async () => {
|
const fetchPostCount = async () => {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const pageRes = await axios.get (`${ API_BASE_URL }/wiki/${ wikiId }`)
|
const pageRes = await axios.get (`${ API_BASE_URL }/wiki/${ wikiId }`)
|
||||||
const pageData: any = pageRes.data
|
const wikiPage = toCamel (pageRes.data as any, { deep: true }) as WikiPage
|
||||||
const wikiPage = toCamel (pageData, { deep: true }) as WikiPage
|
|
||||||
|
|
||||||
const tagRes = await axios.get (`${ API_BASE_URL }/tags/name/${ wikiPage.title }`)
|
const tagRes = await axios.get (`${ API_BASE_URL }/tags/name/${ wikiPage.title }`)
|
||||||
const tagData: any = tagRes.data
|
const tag = toCamel (tagRes.data as any, { deep: true }) as Tag
|
||||||
const tag = toCamel (tagData, { deep: true }) as Tag
|
|
||||||
|
|
||||||
setPostCount (tag.postCount)
|
setPostCount (tag.postCount)
|
||||||
}
|
}
|
||||||
@@ -134,101 +129,47 @@ export default ({ user }: Props) => {
|
|||||||
{
|
{
|
||||||
setPostCount (0)
|
setPostCount (0)
|
||||||
}
|
}
|
||||||
}) ()
|
}
|
||||||
|
fetchPostCount ()
|
||||||
}, [wikiId])
|
}, [wikiId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="bg-gray-800 text-white px-3 flex justify-between items-center w-full min-h-[48px]">
|
<nav className="px-3 flex justify-between items-center w-full min-h-[48px]
|
||||||
|
bg-yellow-50 dark:bg-red-975">
|
||||||
<div className="flex items-center gap-2 h-full">
|
<div className="flex items-center gap-2 h-full">
|
||||||
<Link to="/" className="mx-4 text-xl font-bold text-orange-500">ぼざクリ タグ広場</Link>
|
<Link to="/" className="mx-4 text-xl font-bold
|
||||||
<MyLink to="/posts" title="広場" />
|
text-pink-600 hover:text-pink-400
|
||||||
<MyLink to="/tags" title="タグ" />
|
dark:text-pink-300 dark:hover:text-pink-100">
|
||||||
<MyLink to="/wiki/ヘルプ:ホーム" base="/wiki" title="Wiki" />
|
ぼざクリ タグ広場
|
||||||
<MyLink to="/users/settings" base="/users" title="ニジラー" />
|
</Link>
|
||||||
|
{menu.map (item => (
|
||||||
|
<Link to={item.to}
|
||||||
|
className={cn ('h-full flex items-center',
|
||||||
|
(location.pathname.startsWith (item.base || item.to)
|
||||||
|
? 'bg-yellow-200 dark:bg-red-950 px-4 font-bold'
|
||||||
|
: 'px-2'))}>
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto pr-4">
|
|
||||||
{user && (
|
{user && (
|
||||||
<Button onClick={() => navigate ('/users/settings')}
|
<div className="ml-auto pr-4">
|
||||||
className="bg-gray-600">
|
<Link to="/users/settings"
|
||||||
|
className="font-bold text-red-600 hover:text-red-400
|
||||||
|
dark:text-yellow-400 dark:hover:text-yellow-200">
|
||||||
{user.name || '名もなきニジラー'}
|
{user.name || '名もなきニジラー'}
|
||||||
</Button>)}
|
</Link>
|
||||||
</div>
|
</div>)}
|
||||||
</nav>
|
</nav>
|
||||||
{(() => {
|
<div className={cn ('bg-yellow-200 dark:bg-red-950',
|
||||||
const className = 'bg-gray-700 text-white px-3 flex items-center w-full min-h-[40px]'
|
'text-white px-3 flex items-center w-full min-h-[40px]')}>
|
||||||
const subClass = 'hover:text-orange-500 h-full flex items-center px-3'
|
{menu.find (item => location.pathname.startsWith (item.base || item.to))?.subMenu
|
||||||
// const inputBox = 'flex items-center px-3 mx-2'
|
.filter (item => item.visible ?? true)
|
||||||
const Separator = () => <span className="flex items-center px-2">|</span>
|
.map (item => 'component' in item ? item.component : (
|
||||||
switch (selectedMenu)
|
<Link to={item.to} className="h-full flex items-center px-3">
|
||||||
{
|
{item.name}
|
||||||
case Menu.Post:
|
</Link>))}
|
||||||
return (
|
</div>
|
||||||
<div className={className}>
|
|
||||||
<Link to="/posts" className={subClass}>一覧</Link>
|
|
||||||
<Link to="/posts/new" className={subClass}>投稿追加</Link>
|
|
||||||
<Link to="/wiki/ヘルプ:広場" className={subClass}>ヘルプ</Link>
|
|
||||||
</div>)
|
|
||||||
case Menu.Tag:
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
{/* TODO: リリース後にやる */}
|
|
||||||
{/* <input type="text"
|
|
||||||
className={inputBox}
|
|
||||||
placeholder="タグ検索"
|
|
||||||
value={tagSearch}
|
|
||||||
onChange={whenTagSearchChanged}
|
|
||||||
onFocus={() => setSuggestionsVsbl (true)}
|
|
||||||
onBlur={() => setSuggestionsVsbl (false)}
|
|
||||||
onKeyDown={handleKeyDown} /> */}
|
|
||||||
{/* <Link to="/tags" className={subClass}>タグ</Link> */}
|
|
||||||
{/* <Link to="/tags/aliases" className={subClass}>別名タグ</Link>
|
|
||||||
<Link to="/tags/implications" className={subClass}>上位タグ</Link> */}
|
|
||||||
<Link to="/tags/nico" className={subClass}>ニコニコ連携</Link>
|
|
||||||
<Link to="/wiki/ヘルプ:タグのつけ方" className={subClass}>タグのつけ方</Link>
|
|
||||||
<Link to="/wiki/ヘルプ:タグ" className={subClass}>ヘルプ</Link>
|
|
||||||
</div>)
|
|
||||||
case Menu.Wiki:
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
{/* TODO: リリース後にやる */}
|
|
||||||
{/* <input type="text"
|
|
||||||
className={inputBox}
|
|
||||||
placeholder="Wiki 検索"
|
|
||||||
value={wikiSearch}
|
|
||||||
onChange={whenWikiSearchChanged}
|
|
||||||
onFocus={() => setSuggestionsVsbl (true)}
|
|
||||||
onBlur={() => setSuggestionsVsbl (false)}
|
|
||||||
onKeyDown={handleKeyDown} /> */}
|
|
||||||
<Link to="/wiki" className={subClass}>検索</Link>
|
|
||||||
<Link to="/wiki/new" className={subClass}>新規</Link>
|
|
||||||
<Link to="/wiki/changes" className={subClass}>全体履歴</Link>
|
|
||||||
<Link to="/wiki/ヘルプ:Wiki" className={subClass}>ヘルプ</Link>
|
|
||||||
{(/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId) &&
|
|
||||||
<>
|
|
||||||
<Separator />
|
|
||||||
<Link to={`/posts?tags=${ location.pathname.split ('/')[2] }`} className={subClass}>広場 ({postCount || 0})</Link>
|
|
||||||
<Link to={`/wiki/changes?id=${ wikiId }`} className={subClass}>履歴</Link>
|
|
||||||
<Link to={`/wiki/${ wikiId || location.pathname.split ('/')[2] }/edit`} className={subClass}>編輯</Link>
|
|
||||||
</>}
|
|
||||||
</div>)
|
|
||||||
case Menu.User:
|
|
||||||
return (
|
|
||||||
<div className={className}>
|
|
||||||
{/* TODO: リリース後にやる */}
|
|
||||||
{/* <input type="text"
|
|
||||||
className={inputBox}
|
|
||||||
placeholder="ニジラー検索"
|
|
||||||
value={userSearch}
|
|
||||||
onChange={whenUserSearchChanged}
|
|
||||||
onFocus={() => setSuggestionsVsbl (true)}
|
|
||||||
onBlur={() => setSuggestionsVsbl (false)}
|
|
||||||
onKeyDown={handleKeyDown} /> */}
|
|
||||||
{/* <Link to="/users" className={subClass}>一覧</Link>
|
|
||||||
{user && <Link to={`/users/${ user.id }`} className={subClass}>お前</Link>} */}
|
|
||||||
{user && <Link to="/users/settings" className={subClass}>設定</Link>}
|
|
||||||
</div>)
|
|
||||||
}
|
|
||||||
}) ()}
|
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-3
@@ -1,11 +1,25 @@
|
|||||||
export const CATEGORIES = ['general',
|
import type { Category } from 'types'
|
||||||
'character',
|
|
||||||
'deerjikist',
|
export const LIGHT_COLOUR_SHADE = 800
|
||||||
|
export const DARK_COLOUR_SHADE = 300
|
||||||
|
|
||||||
|
export const CATEGORIES = ['deerjikist',
|
||||||
'meme',
|
'meme',
|
||||||
|
'character',
|
||||||
|
'general',
|
||||||
'material',
|
'material',
|
||||||
'meta',
|
'meta',
|
||||||
'nico'] as const
|
'nico'] as const
|
||||||
|
|
||||||
|
export const TAG_COLOUR = {
|
||||||
|
deerjikist: 'rose',
|
||||||
|
meme: 'purple',
|
||||||
|
character: 'lime',
|
||||||
|
general: 'cyan',
|
||||||
|
material: 'orange',
|
||||||
|
meta: 'yellow',
|
||||||
|
nico: 'gray' } as const satisfies Record<Category, string>
|
||||||
|
|
||||||
export const USER_ROLES = ['admin', 'member', 'guest'] as const
|
export const USER_ROLES = ['admin', 'member', 'guest'] as const
|
||||||
|
|
||||||
export const ViewFlagBehavior = { OnShowedDetail: 1,
|
export const ViewFlagBehavior = { OnShowedDetail: 1,
|
||||||
|
|||||||
+35
-17
@@ -2,13 +2,25 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@layer base {
|
@layer base
|
||||||
body {
|
{
|
||||||
|
body
|
||||||
|
{
|
||||||
@apply overflow-x-clip;
|
@apply overflow-x-clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a
|
||||||
|
{
|
||||||
|
@apply text-blue-600 dark:text-blue-300;
|
||||||
|
}
|
||||||
|
a:hover
|
||||||
|
{
|
||||||
|
@apply text-blue-400 dark:text-blue-100;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root
|
||||||
|
{
|
||||||
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -23,16 +35,14 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a
|
||||||
|
{
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: #646cff;
|
|
||||||
text-decoration: inherit;
|
text-decoration: inherit;
|
||||||
}
|
}
|
||||||
a:hover {
|
|
||||||
color: #535bf2;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body
|
||||||
|
{
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
@@ -40,12 +50,14 @@ body {
|
|||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1
|
||||||
|
{
|
||||||
font-size: 3.2em;
|
font-size: 3.2em;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button
|
||||||
|
{
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
padding: 0.6em 1.2em;
|
padding: 0.6em 1.2em;
|
||||||
@@ -56,23 +68,29 @@ button {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border-color 0.25s;
|
transition: border-color 0.25s;
|
||||||
}
|
}
|
||||||
button:hover {
|
button:hover
|
||||||
|
{
|
||||||
border-color: #646cff;
|
border-color: #646cff;
|
||||||
}
|
}
|
||||||
button:focus,
|
button:focus,
|
||||||
button:focus-visible {
|
button:focus-visible
|
||||||
|
{
|
||||||
outline: 4px auto -webkit-focus-ring-color;
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: light) {
|
@media (prefers-color-scheme: light)
|
||||||
:root {
|
{
|
||||||
|
:root
|
||||||
|
{
|
||||||
color: #213547;
|
color: #213547;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover
|
||||||
|
{
|
||||||
color: #747bff;
|
color: #747bff;
|
||||||
}
|
}
|
||||||
button {
|
button
|
||||||
|
{
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export default ({ user }: Props) => {
|
|||||||
if (!(id))
|
if (!(id))
|
||||||
return
|
return
|
||||||
|
|
||||||
void (async () => {
|
const fetchPost = async () => {
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
const res = await axios.get (`${ API_BASE_URL }/posts/${ id }`, { headers: {
|
const res = await axios.get (`${ API_BASE_URL }/posts/${ id }`, { headers: {
|
||||||
@@ -63,7 +63,8 @@ export default ({ user }: Props) => {
|
|||||||
if (axios.isAxiosError (err))
|
if (axios.isAxiosError (err))
|
||||||
setStatus (err.status ?? 200)
|
setStatus (err.status ?? 200)
|
||||||
}
|
}
|
||||||
}) ()
|
}
|
||||||
|
fetchPost ()
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
|
|||||||
@@ -93,10 +93,10 @@ export default () => {
|
|||||||
<Tab name="広場">
|
<Tab name="広場">
|
||||||
{posts.length
|
{posts.length
|
||||||
? (
|
? (
|
||||||
<div className="flex flex-wrap gap-4 gap-y-8 p-4 justify-between">
|
<div className="flex flex-wrap gap-6 p-4">
|
||||||
{posts.map ((post, i) => (
|
{posts.map ((post, i) => (
|
||||||
<Link to={`/posts/${ post.id }`}
|
<Link to={`/posts/${ post.id }`}
|
||||||
key={post.id}
|
key={i}
|
||||||
className="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg">
|
className="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg">
|
||||||
<img src={post.thumbnail || post.thumbnailBase || undefined}
|
<img src={post.thumbnail || post.thumbnailBase || undefined}
|
||||||
alt={post.title || post.url}
|
alt={post.title || post.url}
|
||||||
@@ -115,7 +115,7 @@ export default () => {
|
|||||||
<Tab name="Wiki" init={!(posts.length)}>
|
<Tab name="Wiki" init={!(posts.length)}>
|
||||||
<WikiBody body={wikiPage.body} />
|
<WikiBody body={wikiPage.body} />
|
||||||
<div className="my-2">
|
<div className="my-2">
|
||||||
<Link to={`/wiki/${ wikiPage.title }`}>Wiki を見る</Link>
|
<Link to={`/wiki/${ encodeURIComponent (wikiPage.title) }`}>Wiki を見る</Link>
|
||||||
</div>
|
</div>
|
||||||
</Tab>)}
|
</Tab>)}
|
||||||
</TabGroup>
|
</TabGroup>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import toCamel from 'camelcase-keys'
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
|
|
||||||
|
import TagLink from '@/components/TagLink'
|
||||||
import SectionTitle from '@/components/common/SectionTitle'
|
import SectionTitle from '@/components/common/SectionTitle'
|
||||||
import TextArea from '@/components/common/TextArea'
|
import TextArea from '@/components/common/TextArea'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
@@ -15,10 +16,10 @@ type Props = { user: User | null }
|
|||||||
|
|
||||||
|
|
||||||
export default ({ user }: Props) => {
|
export default ({ user }: Props) => {
|
||||||
const [nicoTags, setNicoTags] = useState<NicoTag[]> ([])
|
|
||||||
const [cursor, setCursor] = useState ('')
|
const [cursor, setCursor] = useState ('')
|
||||||
const [loading, setLoading] = useState (false)
|
|
||||||
const [editing, setEditing] = useState<{ [key: number]: boolean }> ({ })
|
const [editing, setEditing] = useState<{ [key: number]: boolean }> ({ })
|
||||||
|
const [loading, setLoading] = useState (false)
|
||||||
|
const [nicoTags, setNicoTags] = useState<NicoTag[]> ([])
|
||||||
const [rawTags, setRawTags] = useState<{ [key: number]: string }> ({ })
|
const [rawTags, setRawTags] = useState<{ [key: number]: string }> ({ })
|
||||||
|
|
||||||
const loaderRef = useRef<HTMLDivElement | null> (null)
|
const loaderRef = useRef<HTMLDivElement | null> (null)
|
||||||
@@ -56,6 +57,10 @@ export default ({ user }: Props) => {
|
|||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
'X-Transfer-Code': localStorage.getItem ('user_code') ?? '' } })
|
||||||
const data = toCamel (res.data as any, { deep: true }) as Tag[]
|
const data = toCamel (res.data as any, { deep: true }) as Tag[]
|
||||||
|
setNicoTags (nicoTags => {
|
||||||
|
nicoTags.find (t => t.id === id)!.linkedTags = data
|
||||||
|
return [...nicoTags]
|
||||||
|
})
|
||||||
setRawTags (rawTags => ({ ...rawTags, [id]: data.map (t => t.name).join (' ') }))
|
setRawTags (rawTags => ({ ...rawTags, [id]: data.map (t => t.name).join (' ') }))
|
||||||
|
|
||||||
toast ({ title: '更新しました.' })
|
toast ({ title: '更新しました.' })
|
||||||
@@ -106,13 +111,10 @@ export default ({ user }: Props) => {
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{nicoTags.map (tag => (
|
{nicoTags.map ((tag, i) => (
|
||||||
<tr key={tag.id}>
|
<tr key={i}>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
<a href={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
|
<TagLink tag={tag} withWiki={false} withCount={false} />
|
||||||
target="_blank">
|
|
||||||
{tag.name}
|
|
||||||
</a>
|
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
{editing[tag.id]
|
{editing[tag.id]
|
||||||
@@ -120,7 +122,12 @@ export default ({ user }: Props) => {
|
|||||||
<TextArea value={rawTags[tag.id]} onChange={ev => {
|
<TextArea value={rawTags[tag.id]} onChange={ev => {
|
||||||
setRawTags (rawTags => ({ ...rawTags, [tag.id]: ev.target.value }))
|
setRawTags (rawTags => ({ ...rawTags, [tag.id]: ev.target.value }))
|
||||||
}} />)
|
}} />)
|
||||||
: rawTags[tag.id]}
|
: tag.linkedTags.map((lt, j) => (
|
||||||
|
<span key={j} className="mr-2">
|
||||||
|
<TagLink tag={lt}
|
||||||
|
linkFlg={false}
|
||||||
|
withCount={false} />
|
||||||
|
</span>))}
|
||||||
</td>
|
</td>
|
||||||
{memberFlg && (
|
{memberFlg && (
|
||||||
<td>
|
<td>
|
||||||
@@ -129,7 +136,11 @@ export default ({ user }: Props) => {
|
|||||||
handleEdit (tag.id)
|
handleEdit (tag.id)
|
||||||
}}>
|
}}>
|
||||||
{editing[tag.id]
|
{editing[tag.id]
|
||||||
? <span className="text-red-400">更新</span>
|
? (
|
||||||
|
<span className="text-red-600 hover:text-red-400
|
||||||
|
dark:text-red-300 dark:hover:text-red-100">
|
||||||
|
更新
|
||||||
|
</span>)
|
||||||
: <span>編輯</span>}
|
: <span>編輯</span>}
|
||||||
</a>
|
</a>
|
||||||
</td>)}
|
</td>)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useLocation, useParams } from 'react-router-dom'
|
|||||||
import PageTitle from '@/components/common/PageTitle'
|
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'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import type { WikiPageDiff } from '@/types'
|
import type { WikiPageDiff } from '@/types'
|
||||||
|
|
||||||
@@ -36,11 +37,14 @@ export default () => {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
<PageTitle>{diff?.title}</PageTitle>
|
<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 => (
|
||||||
<span className={d.type === 'added' ? 'bg-green-800' : d.type === 'removed' ? 'bg-red-800' : ''}>
|
<span className={cn (d.type === 'added' && 'bg-green-200 dark:bg-green-800',
|
||||||
|
d.type === 'removed' && 'bg-red-200 dark:bg-red-800')}>
|
||||||
{d.content == '\n' ? <br /> : d.content}
|
{d.content == '\n' ? <br /> : d.content}
|
||||||
</span>))) : 'Loading...'}
|
</span>)))
|
||||||
|
: 'Loading...'}
|
||||||
</div>
|
</div>
|
||||||
</MainArea>)
|
</MainArea>)
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-2
@@ -1,10 +1,19 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
import { CATEGORIES, USER_ROLES, ViewFlagBehavior } from '@/consts'
|
import { CATEGORIES, USER_ROLES, ViewFlagBehavior } from '@/consts'
|
||||||
|
|
||||||
export type Category = typeof CATEGORIES[number]
|
export type Category = typeof CATEGORIES[number]
|
||||||
|
|
||||||
export type NicoTag = {
|
export type Menu = MenuItem[]
|
||||||
id: number
|
|
||||||
|
export type MenuItem = {
|
||||||
name: string
|
name: string
|
||||||
|
to: string
|
||||||
|
base?: string
|
||||||
|
subMenu: SubMenuItem[] }
|
||||||
|
|
||||||
|
export type NicoTag = Tag & {
|
||||||
|
category: 'nico'
|
||||||
linkedTags: Tag[] }
|
linkedTags: Tag[] }
|
||||||
|
|
||||||
export type Post = {
|
export type Post = {
|
||||||
@@ -16,6 +25,14 @@ export type Post = {
|
|||||||
tags: Tag[]
|
tags: Tag[]
|
||||||
viewed: boolean }
|
viewed: boolean }
|
||||||
|
|
||||||
|
export type SubMenuItem = {
|
||||||
|
component: React.ReactNode
|
||||||
|
visible: boolean
|
||||||
|
} | {
|
||||||
|
name: string
|
||||||
|
to: string
|
||||||
|
visible?: boolean }
|
||||||
|
|
||||||
export type Tag = {
|
export type Tag = {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -1,20 +1,35 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
import type { Config } from 'tailwindcss'
|
import type { Config } from 'tailwindcss'
|
||||||
|
|
||||||
|
import { DARK_COLOUR_SHADE,
|
||||||
|
LIGHT_COLOUR_SHADE,
|
||||||
|
TAG_COLOUR } from './src/consts'
|
||||||
|
|
||||||
|
const colours = Object.values (TAG_COLOUR)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: ['./index.html',
|
content: ['./src/**/*.{html,js,ts,jsx,tsx}'],
|
||||||
'./src/**/*.{js,ts,jsx,tsx}'],
|
safelist: [...colours.map (c => `text-${ c }-${ LIGHT_COLOUR_SHADE }`),
|
||||||
|
...colours.map (c => `hover:text-${ c }-${ LIGHT_COLOUR_SHADE - 200 }`),
|
||||||
|
...colours.map (c => `dark:text-${ c }-${ DARK_COLOUR_SHADE }`),
|
||||||
|
...colours.map (c => `dark:hover:text-${ c }-${ DARK_COLOUR_SHADE - 200 }`)],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
|
animation: {
|
||||||
|
'rainbow-scroll': 'rainbow-scroll .25s linear infinite',
|
||||||
|
},
|
||||||
|
colors: {
|
||||||
|
red: {
|
||||||
|
925: '#5f1414',
|
||||||
|
975: '#230505',
|
||||||
|
}
|
||||||
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
'rainbow-scroll': {
|
'rainbow-scroll': {
|
||||||
'0%': { backgroundPosition: '0% 50%' },
|
'0%': { backgroundPosition: '0% 50%' },
|
||||||
'100%': { backgroundPosition: '200% 50%' },
|
'100%': { backgroundPosition: '200% 50%' },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
animation: {
|
|
||||||
'rainbow-scroll': 'rainbow-scroll .25s linear infinite',
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする