コミットを比較

..

5 コミット

作成者 SHA1 メッセージ 日付
みてるぞ 4ec9c9c5e0 Merge branch 'main' into #23 2025-07-21 17:31:57 +09:00
みてるぞ 5a4919116d #23 2025-07-21 16:54:30 +09:00
みてるぞ 69d111a133 #23 2025-07-21 05:21:52 +09:00
みてるぞ b3405a2d8d Merge branch 'main' into #23 2025-07-21 05:13:51 +09:00
みてるぞ 3f42eb6915 #23 2025-07-21 05:11:14 +09:00
13個のファイルの変更304行の追加274行の削除
-42
ファイルの表示
@@ -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;
}
+13 -12
ファイルの表示
@@ -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'
@@ -16,29 +16,32 @@ 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>))}
+48
ファイルの表示
@@ -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>)}
</>)
}
+7 -5
ファイルの表示
@@ -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>
+72 -131
ファイルの表示
@@ -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;
} }
} }
+3 -2
ファイルの表示
@@ -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 (() => {
+3 -3
ファイルの表示
@@ -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>
+21 -10
ファイルの表示
@@ -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 -3
ファイルの表示
@@ -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
+20 -5
ファイルの表示
@@ -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: [],