@@ -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,47 +14,48 @@ 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 }> = { | |||||
general: '一般', | |||||
const categoryNames: Record<Category, string> = { | |||||
deerjikist: 'ニジラー', | deerjikist: 'ニジラー', | ||||
meme: '原作・ネタ元・ミーム等', | |||||
character: 'キャラクター', | |||||
general: '一般', | |||||
material: '素材', | |||||
meta: 'メタタグ', | |||||
nico: 'ニコニコタグ' } | nico: 'ニコニコタグ' } | ||||
useEffect (() => { | useEffect (() => { | ||||
if (!(post)) | if (!(post)) | ||||
return | return | ||||
const fetchTags = () => { | |||||
const tagsTmp = { } as TagByCategory | |||||
for (const tag of post.tags) | |||||
{ | |||||
if (!(tag.category in tagsTmp)) | |||||
tagsTmp[tag.category] = [] | |||||
tagsTmp[tag.category].push (tag) | |||||
} | |||||
for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[]) | |||||
tagsTmp[cat].sort ((tagA: Tag, tagB: Tag) => tagA.name < tagB.name ? -1 : 1) | |||||
setTags (tagsTmp) | |||||
} | |||||
fetchTags () | |||||
const tagsTmp = { } as TagByCategory | |||||
for (const tag of post.tags) | |||||
{ | |||||
if (!(tag.category in tagsTmp)) | |||||
tagsTmp[tag.category] = [] | |||||
tagsTmp[tag.category].push (tag) | |||||
} | |||||
for (const cat of Object.keys (tagsTmp) as (keyof typeof tagsTmp)[]) | |||||
tagsTmp[cat].sort ((tagA: Tag, tagB: Tag) => tagA.name < tagB.name ? -1 : 1) | |||||
setTags (tagsTmp) | |||||
}, [post]) | }, [post]) | ||||
return ( | return ( | ||||
<SidebarComponent> | <SidebarComponent> | ||||
<TagSearch /> | |||||
{CATEGORIES.map ((cat: Category) => cat in tags && ( | |||||
<div className="my-3" key={cat}> | |||||
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle> | |||||
<ul> | |||||
{tags[cat].map (tag => ( | |||||
<li key={tag.id} className="mb-1"> | |||||
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}> | |||||
{tag.name} | |||||
</Link> | |||||
</li>))} | |||||
</ul> | |||||
</div>))} | |||||
<TagSearch /> | |||||
{CATEGORIES.map ((cat: Category) => cat in tags && ( | |||||
<div className="my-3" key={cat}> | |||||
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle> | |||||
<ul> | |||||
{tags[cat].map ((tag, i) => ( | |||||
<li key={i} className="mb-1"> | |||||
<TagLink tag={tag} /> | |||||
</li>))} | |||||
</ul> | |||||
</div>))} | |||||
</SidebarComponent>) | </SidebarComponent>) | ||||
} | } |
@@ -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 () }`}> | |||||
{tag.name} | |||||
</Link> | |||||
<span className="ml-1">{tag.postCount}</span> | |||||
<TagLink tag={tag} /> | |||||
</li>))) : [])} | </li>))) : [])} | ||||
</ul> | </ul> | ||||
<SectionTitle>関聯</SectionTitle> | <SectionTitle>関聯</SectionTitle> | ||||
@@ -17,7 +17,6 @@ const Menu = { None: 'None', | |||||
User: 'User', | User: 'User', | ||||
Tag: 'Tag', | Tag: 'Tag', | ||||
Wiki: 'Wiki' } as const | Wiki: 'Wiki' } as const | ||||
type Menu = typeof Menu[keyof typeof Menu] | type Menu = typeof Menu[keyof typeof Menu] | ||||
@@ -38,9 +37,9 @@ export default ({ user }: Props) => { | |||||
const MyLink = ({ to, title, base }: { to: string | const MyLink = ({ to, title, base }: { to: string | ||||
title: string | title: string | ||||
base?: string }) => ( | base?: string }) => ( | ||||
<Link to={to} className={cn ('hover:text-orange-500 h-full flex items-center', | |||||
<Link to={to} className={cn ('h-full flex items-center', | |||||
(location.pathname.startsWith (base ?? to) | (location.pathname.startsWith (base ?? to) | ||||
? 'bg-gray-700 px-4 font-bold' | |||||
? 'bg-yellow-200 dark:bg-red-950 px-4 font-bold' | |||||
: 'px-2'))}> | : 'px-2'))}> | ||||
{title} | {title} | ||||
</Link>) | </Link>) | ||||
@@ -121,11 +120,9 @@ export default ({ user }: Props) => { | |||||
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 (pageData, { deep: true }) as WikiPage | |||||
const wikiPage = toCamel (pageRes.data as any, { 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 (tagData, { deep: true }) as Tag | const tag = toCamel (tagData, { deep: true }) as Tag | ||||
setPostCount (tag.postCount) | setPostCount (tag.postCount) | ||||
@@ -139,25 +136,32 @@ export default ({ user }: Props) => { | |||||
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 | |||||
text-pink-600 hover:text-pink-400 | |||||
dark:text-pink-300 dark:hover:text-pink-100"> | |||||
ぼざクリ タグ広場 | |||||
</Link> | |||||
<MyLink to="/posts" title="広場" /> | <MyLink to="/posts" title="広場" /> | ||||
<MyLink to="/tags" title="タグ" /> | <MyLink to="/tags" title="タグ" /> | ||||
<MyLink to="/wiki/ヘルプ:ホーム" base="/wiki" title="Wiki" /> | <MyLink to="/wiki/ヘルプ:ホーム" base="/wiki" title="Wiki" /> | ||||
<MyLink to="/users/settings" base="/users" title="ニジラー" /> | <MyLink to="/users/settings" base="/users" title="ニジラー" /> | ||||
</div> | </div> | ||||
<div className="ml-auto pr-4"> | |||||
{user && ( | |||||
<Button onClick={() => navigate ('/users/settings')} | |||||
className="bg-gray-600"> | |||||
{user && | |||||
<div className="ml-auto pr-4"> | |||||
<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>)} | |||||
</div> | |||||
</Link> | |||||
</div>} | |||||
</nav> | </nav> | ||||
{(() => { | {(() => { | ||||
const className = 'bg-gray-700 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' | |||||
const className = cn ('bg-yellow-200 dark:bg-red-950', | |||||
'text-white px-3 flex items-center w-full min-h-[40px]') | |||||
const subClass = 'h-full flex items-center px-3' | |||||
// const inputBox = 'flex items-center px-3 mx-2' | // const inputBox = 'flex items-center px-3 mx-2' | ||||
const Separator = () => <span className="flex items-center px-2">|</span> | const Separator = () => <span className="flex items-center px-2">|</span> | ||||
switch (selectedMenu) | switch (selectedMenu) | ||||
@@ -1,11 +1,25 @@ | |||||
export const CATEGORIES = ['general', | |||||
'character', | |||||
'deerjikist', | |||||
import type { Category } from 'types' | |||||
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, | ||||
@@ -2,13 +2,25 @@ | |||||
@tailwind components; | @tailwind components; | ||||
@tailwind utilities; | @tailwind utilities; | ||||
@layer base { | |||||
body { | |||||
@layer base | |||||
{ | |||||
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) { | |||||
:root { | |||||
@media (prefers-color-scheme: light) | |||||
{ | |||||
: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,16 +93,16 @@ export default () => { | |||||
<Tab name="広場"> | <Tab name="広場"> | ||||
{posts.length | {posts.length | ||||
? ( | ? ( | ||||
<div className="flex flex-wrap gap-4 gap-y-8 p-4 justify-between"> | |||||
{posts.map (post => ( | |||||
<Link to={`/posts/${ post.id }`} | |||||
key={post.id} | |||||
className="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg"> | |||||
<img src={post.thumbnail || post.thumbnailBase || undefined} | |||||
alt={post.title || post.url} | |||||
title={post.title || post.url || undefined} | |||||
className="object-none w-full h-full" /> | |||||
</Link>))} | |||||
<div className="flex flex-wrap gap-6 p-4"> | |||||
{posts.map ((post, i) => ( | |||||
<Link to={`/posts/${ post.id }`} | |||||
key={i} | |||||
className="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg"> | |||||
<img src={post.thumbnail || post.thumbnailBase || undefined} | |||||
alt={post.title || post.url} | |||||
title={post.title || post.url || undefined} | |||||
className="object-none w-full h-full" /> | |||||
</Link>))} | |||||
</div>) | </div>) | ||||
: !(loading) && '広場には何もありませんよ.'} | : !(loading) && '広場には何もありませんよ.'} | ||||
{loading && 'Loading...'} | {loading && 'Loading...'} | ||||
@@ -112,7 +112,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 => ( | |||||
<tr key={tag.id}> | |||||
{nicoTags.map ((tag, i) => ( | |||||
<tr key={i}> | |||||
<td className="p-2"> | <td className="p-2"> | ||||
<a href={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`} | |||||
target="_blank"> | |||||
{tag.name} | |||||
</a> | |||||
<TagLink tag={tag} withWiki={false} withCount={false} /> | |||||
</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> | ||||
@@ -2,9 +2,8 @@ import { CATEGORIES, USER_ROLES, ViewFlagBehavior } from '@/consts' | |||||
export type Category = typeof CATEGORIES[number] | export type Category = typeof CATEGORIES[number] | ||||
export type NicoTag = { | |||||
id: number | |||||
name: string | |||||
export type NicoTag = Tag & { | |||||
category: 'nico' | |||||
linkedTags: Tag[] } | linkedTags: Tag[] } | ||||
export type Post = { | export type Post = { | ||||
@@ -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', | |||||
'./src/**/*.{js,ts,jsx,tsx}'], | |||||
content: ['./src/**/*.{html,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: [], | ||||