コミットを比較
7 コミット
4ec9c9c5e0
...
a46d15467e
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| a46d15467e | |||
| e907cb0a8f | |||
| 8222ed233e | |||
| 64133063bb | |||
| 91a393d20c | |||
| da594e6637 | |||
| 2447601b1d |
@@ -0,0 +1,12 @@
|
|||||||
|
namespace :nico do
|
||||||
|
desc 'ニコタグ連携'
|
||||||
|
task link: :environment do
|
||||||
|
Post.find_each do |post|
|
||||||
|
tags = post.tags.where(category: 'nico')
|
||||||
|
tags.each do |tag|
|
||||||
|
post.tags.concat(tag.linked_tags) if tag.linked_tags.present?
|
||||||
|
end
|
||||||
|
post.tags = post.tags.to_a.uniq
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -31,7 +31,7 @@ export default ({ status }: Props) => {
|
|||||||
style.innerHTML = `
|
style.innerHTML = `
|
||||||
@font-face
|
@font-face
|
||||||
{
|
{
|
||||||
font-family: 'MyFont';
|
font-family: 'Nikumaru';
|
||||||
src: url(${ nikumaru }) format('opentype');
|
src: url(${ nikumaru }) format('opentype');
|
||||||
}`
|
}`
|
||||||
document.head.appendChild (style)
|
document.head.appendChild (style)
|
||||||
@@ -42,15 +42,15 @@ export default ({ status }: Props) => {
|
|||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
<title>{title} | {SITE_TITLE}</title>
|
<title>{title} | {SITE_TITLE}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
<div className="text-6xl font-bold text-transparent whitespace-nowrap
|
<div className="text-2xl sm:text-4xl md:text-6xl font-bold text-transparent
|
||||||
bg-[linear-gradient(90deg,#ff0000,#ff8800,#ffff00,#00ff00,#00ffff,#0000ff,#ff00ff,#ff0000)]
|
bg-[linear-gradient(90deg,#ff0000,#ff8800,#ffff00,#00ff00,#00ffff,#0000ff,#ff00ff,#ff0000)]
|
||||||
bg-clip-text bg-[length:200%_100%] animate-rainbow-scroll drop-shadow-[0_0_6px_black]
|
bg-clip-text bg-[length:200%_100%] animate-rainbow-scroll drop-shadow-[0_0_6px_black]
|
||||||
space-y-6 text-center flex flex-col justify-center items-center"
|
space-y-2 sm:space-y-4 md:space-y-6 text-center flex flex-col justify-center items-center"
|
||||||
style={{ fontFamily: 'MyFont' }}>
|
style={{ fontFamily: 'Nikumaru' }}>
|
||||||
<p>{status}</p>
|
<p>{status}</p>
|
||||||
<div className="flex space-x-4">
|
<div className="flex flex-row space-x-1 sm:space-x-2 md:space-x-4">
|
||||||
<p style={{ writingMode: 'vertical-rl' }}>{leftMsg}</p>
|
<p style={{ writingMode: 'vertical-rl' }}>{leftMsg}</p>
|
||||||
<img src={errorImg} alt="逃げたギター" />
|
<img className="max-w-[70vw]" src={errorImg} alt="逃げたギター" />
|
||||||
<p style={{ writingMode: 'vertical-rl' }}>{rightMsg}</p>
|
<p style={{ writingMode: 'vertical-rl' }}>{rightMsg}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="mr-[-.5em]">{message}</p>
|
<p className="mr-[-.5em]">{message}</p>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default () => (
|
||||||
|
<>
|
||||||
|
<span className="hidden md:inline flex items-center px-2">|</span>
|
||||||
|
<hr className="block md:hidden w-full opacity-25
|
||||||
|
border-t border-black dark:border-white" />
|
||||||
|
</>)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import toCamel from 'camelcase-keys'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
import TextArea from '@/components/common/TextArea'
|
import TextArea from '@/components/common/TextArea'
|
||||||
@@ -22,7 +23,7 @@ export default ({ post, onSave }: Props) => {
|
|||||||
const res = await axios.put (`${ API_BASE_URL }/posts/${ post.id }`, { title, tags },
|
const res = await axios.put (`${ API_BASE_URL }/posts/${ post.id }`, { title, tags },
|
||||||
{ headers: { 'Content-Type': 'multipart/form-data',
|
{ headers: { 'Content-Type': 'multipart/form-data',
|
||||||
'X-Transfer-Code': localStorage.getItem ('user_code') || '' } } )
|
'X-Transfer-Code': localStorage.getItem ('user_code') || '' } } )
|
||||||
const data = res.data as Post
|
const data = toCamel (res.data as any, { deep: true }) as Post
|
||||||
onSave ({ ...post,
|
onSave ({ ...post,
|
||||||
title: data.title,
|
title: data.title,
|
||||||
tags: data.tags } as Post)
|
tags: data.tags } as Post)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
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 } from 'react-router-dom'
|
||||||
|
|
||||||
|
import Separator from '@/components/MenuSeparator'
|
||||||
|
import TopNavUser from '@/components/TopNavUser'
|
||||||
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'
|
||||||
@@ -14,16 +16,11 @@ type Props = { user: User | null }
|
|||||||
|
|
||||||
export default ({ user }: Props) => {
|
export default ({ user }: Props) => {
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
// const navigate = useNavigate ()
|
|
||||||
|
|
||||||
// const [activeIndex, setActiveIndex] = useState (-1)
|
const [menuOpen, setMenuOpen] = useState (false)
|
||||||
|
const [openItemIdx, setOpenItemIdx] = useState (-1)
|
||||||
const [postCount, setPostCount] = useState<number | null> (null)
|
const [postCount, setPostCount] = useState<number | null> (null)
|
||||||
// const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
|
|
||||||
// const [suggestions, setSuggestions] = useState<WikiPage[]> ([])
|
|
||||||
// const [tagSearch, setTagSearch] = useState ('')
|
|
||||||
// const [userSearch, setUserSearch] = useState ('')
|
|
||||||
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
|
const [wikiId, setWikiId] = useState<number | null> (WikiIdBus.get ())
|
||||||
// const [wikiSearch, setWikiSearch] = useState ('')
|
|
||||||
|
|
||||||
const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId)
|
const wikiPageFlg = Boolean (/^\/wiki\/(?!new|changes)[^\/]+/.test (location.pathname) && wikiId)
|
||||||
const wikiTitle = location.pathname.split ('/')[2]
|
const wikiTitle = location.pathname.split ('/')[2]
|
||||||
@@ -36,7 +33,7 @@ export default ({ user }: Props) => {
|
|||||||
{ name: 'タグ一覧', to: '/tags', visible: false },
|
{ name: 'タグ一覧', to: '/tags', visible: false },
|
||||||
{ name: '別名タグ', to: '/tags/aliases', visible: false },
|
{ name: '別名タグ', to: '/tags/aliases', visible: false },
|
||||||
{ name: '上位タグ', to: '/tags/implications', visible: false },
|
{ name: '上位タグ', to: '/tags/implications', visible: false },
|
||||||
{ name: 'ニコニコ連携', to: '/tags/posts' },
|
{ name: 'ニコニコ連携', to: '/tags/nico' },
|
||||||
{ name: 'タグのつけ方', to: '/wiki/ヘルプ:タグのつけ方' },
|
{ name: 'タグのつけ方', to: '/wiki/ヘルプ:タグのつけ方' },
|
||||||
{ name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' }] },
|
{ name: 'ヘルプ', to: '/wiki/ヘルプ:タグ' }] },
|
||||||
{ name: 'Wiki', to: '/wiki/ヘルプ:ホーム', base: '/wiki', subMenu: [
|
{ name: 'Wiki', to: '/wiki/ヘルプ:ホーム', base: '/wiki', subMenu: [
|
||||||
@@ -44,8 +41,7 @@ export default ({ user }: Props) => {
|
|||||||
{ name: '新規', to: '/wiki/new' },
|
{ name: '新規', to: '/wiki/new' },
|
||||||
{ name: '全体履歴', to: '/wiki/changes' },
|
{ name: '全体履歴', to: '/wiki/changes' },
|
||||||
{ name: 'ヘルプ', to: '/wiki/ヘルプ:Wiki' },
|
{ name: 'ヘルプ', to: '/wiki/ヘルプ:Wiki' },
|
||||||
{ component: <span className="flex items-center px-2">|</span>,
|
{ component: <Separator />, visible: wikiPageFlg },
|
||||||
visible: wikiPageFlg },
|
|
||||||
{ name: `広場 (${ postCount || 0 })`, to: `/posts?tags=${ wikiTitle }`,
|
{ name: `広場 (${ postCount || 0 })`, to: `/posts?tags=${ wikiTitle }`,
|
||||||
visible: wikiPageFlg },
|
visible: wikiPageFlg },
|
||||||
{ name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
|
{ name: '履歴', to: `/wiki/changes?id=${ wikiId }`, visible: wikiPageFlg },
|
||||||
@@ -55,61 +51,17 @@ export default ({ user }: Props) => {
|
|||||||
{ name: 'お前', to: `/users/${ user?.id }`, visible: false },
|
{ name: 'お前', to: `/users/${ user?.id }`, visible: false },
|
||||||
{ name: '設定', to: '/users/settings', visible: Boolean (user) }] }]
|
{ name: '設定', to: '/users/settings', visible: Boolean (user) }] }]
|
||||||
|
|
||||||
// const whenTagSearchChanged = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
// // TODO: 実装
|
|
||||||
//
|
|
||||||
// setTagSearch (ev.target.value)
|
|
||||||
//
|
|
||||||
// const q: string = ev.target.value.split (' ').at (-1)
|
|
||||||
// if (!(q))
|
|
||||||
// {
|
|
||||||
// // setSuggestions ([])
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const whenWikiSearchChanged = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
// // TODO: 実装
|
|
||||||
//
|
|
||||||
// setWikiSearch (ev.target.value)
|
|
||||||
//
|
|
||||||
// const q: string = ev.target.value.split (' ').at (-1)
|
|
||||||
// if (!(q))
|
|
||||||
// {
|
|
||||||
// // setSuggestions ([])
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const whenUserSearchChanged = (ev: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
// // TODO: 実装
|
|
||||||
//
|
|
||||||
// setUserSearch (ev.target.value)
|
|
||||||
//
|
|
||||||
// const q: string = ev.target.value.split (' ').at (-1)
|
|
||||||
// if (!(q))
|
|
||||||
// {
|
|
||||||
// // setSuggestions ([])
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const handleKeyDown = (ev: React.KeyboardEvent<HTMLInputElement>) => {
|
|
||||||
// if (ev.key === 'Enter' && wikiSearch.length && (!(suggestionsVsbl) || activeIndex < 0))
|
|
||||||
// {
|
|
||||||
// navigate (`/wiki/${ encodeURIComponent (wikiSearch) }`)
|
|
||||||
// setSuggestionsVsbl (false)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const handleTagSelect = (tag: Tag) => {
|
|
||||||
// }
|
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
const unsubscribe = WikiIdBus.subscribe (setWikiId)
|
const unsubscribe = WikiIdBus.subscribe (setWikiId)
|
||||||
return () => unsubscribe ()
|
return () => unsubscribe ()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
setMenuOpen (false)
|
||||||
|
setOpenItemIdx (menu.findIndex (item => (
|
||||||
|
location.pathname.startsWith (item.base || item.to))))
|
||||||
|
}, [location])
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
if (!(wikiId))
|
if (!(wikiId))
|
||||||
return
|
return
|
||||||
@@ -136,16 +88,18 @@ export default ({ user }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="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">
|
bg-yellow-200 dark:bg-red-975 md:bg-yellow-50">
|
||||||
<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
|
<Link to="/"
|
||||||
text-pink-600 hover:text-pink-400
|
className="mx-4 text-xl font-bold text-pink-600 hover:text-pink-400
|
||||||
dark:text-pink-300 dark:hover:text-pink-100">
|
dark:text-pink-300 dark:hover:text-pink-100">
|
||||||
ぼざクリ タグ広場
|
ぼざクリ タグ広場
|
||||||
</Link>
|
</Link>
|
||||||
{menu.map (item => (
|
|
||||||
<Link to={item.to}
|
{menu.map ((item, i) => (
|
||||||
className={cn ('h-full flex items-center',
|
<Link key={i}
|
||||||
|
to={item.to}
|
||||||
|
className={cn ('hidden md:flex h-full items-center',
|
||||||
(location.pathname.startsWith (item.base || item.to)
|
(location.pathname.startsWith (item.base || item.to)
|
||||||
? 'bg-yellow-200 dark:bg-red-950 px-4 font-bold'
|
? 'bg-yellow-200 dark:bg-red-950 px-4 font-bold'
|
||||||
: 'px-2'))}>
|
: 'px-2'))}>
|
||||||
@@ -153,23 +107,65 @@ export default ({ user }: Props) => {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{user && (
|
|
||||||
<div className="ml-auto pr-4">
|
<TopNavUser user={user} />
|
||||||
<Link to="/users/settings"
|
|
||||||
className="font-bold text-red-600 hover:text-red-400
|
<a href="#"
|
||||||
dark:text-yellow-400 dark:hover:text-yellow-200">
|
className="md:hidden ml-auto pr-4
|
||||||
{user.name || '名もなきニジラー'}
|
text-pink-600 hover:text-pink-400
|
||||||
</Link>
|
dark:text-pink-300 dark:hover:text-pink-100"
|
||||||
</div>)}
|
onClick={ev => {
|
||||||
|
ev.preventDefault ()
|
||||||
|
setMenuOpen (!(menuOpen))
|
||||||
|
}}>
|
||||||
|
{menuOpen ? '×' : 'Menu'}
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
<div className={cn ('bg-yellow-200 dark:bg-red-950',
|
|
||||||
'text-white px-3 flex items-center w-full min-h-[40px]')}>
|
<div className="hidden md:flex bg-yellow-200 dark:bg-red-950
|
||||||
|
items-center w-full min-h-[40px] px-3">
|
||||||
{menu.find (item => location.pathname.startsWith (item.base || item.to))?.subMenu
|
{menu.find (item => location.pathname.startsWith (item.base || item.to))?.subMenu
|
||||||
.filter (item => item.visible ?? true)
|
.filter (item => item.visible ?? true)
|
||||||
.map (item => 'component' in item ? item.component : (
|
.map ((item, i) => 'component' in item ? item.component : (
|
||||||
<Link to={item.to} className="h-full flex items-center px-3">
|
<Link key={i}
|
||||||
|
to={item.to}
|
||||||
|
className="h-full flex items-center px-3">
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>))}
|
</Link>))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className={cn (menuOpen ? 'flex flex-col md:hidden' : 'hidden',
|
||||||
|
'bg-yellow-200 dark:bg-red-975 items-start')}>
|
||||||
|
<Separator />
|
||||||
|
{menu.map ((item, i) => (
|
||||||
|
<>
|
||||||
|
<Link key={i}
|
||||||
|
to={i === openItemIdx ? item.to : '#'}
|
||||||
|
className={cn ('w-full min-h-[40px] flex items-center pl-8',
|
||||||
|
((i === openItemIdx)
|
||||||
|
&& 'font-bold bg-yellow-50 dark:bg-red-950'))}
|
||||||
|
onClick={ev => {
|
||||||
|
if (i !== openItemIdx)
|
||||||
|
{
|
||||||
|
ev.preventDefault ()
|
||||||
|
setOpenItemIdx (i)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
{i === openItemIdx && (
|
||||||
|
item.subMenu
|
||||||
|
.filter (subItem => subItem.visible ?? true)
|
||||||
|
.map ((subItem, j) => 'component' in subItem ? subItem.component : (
|
||||||
|
<Link key={j}
|
||||||
|
to={subItem.to}
|
||||||
|
className="w-full min-h-[36px] flex items-center pl-12
|
||||||
|
bg-yellow-50 dark:bg-red-950">
|
||||||
|
{subItem.name}
|
||||||
|
</Link>)))}
|
||||||
|
</>))}
|
||||||
|
<TopNavUser user={user} sp />
|
||||||
|
<Separator />
|
||||||
|
</div>
|
||||||
</>)
|
</>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
import Separator from '@/components/MenuSeparator'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
import type { User } from '@/types'
|
||||||
|
|
||||||
|
type Props = { user: User | null,
|
||||||
|
sp?: boolean }
|
||||||
|
|
||||||
|
|
||||||
|
export default ({ user, sp }: Props) => {
|
||||||
|
if (!(user))
|
||||||
|
return
|
||||||
|
|
||||||
|
const className = cn ((sp
|
||||||
|
? 'md:hidden w-full min-h-[40px] flex items-center pl-8'
|
||||||
|
: 'hidden md:block ml-auto pr-4'),
|
||||||
|
'font-bold text-red-600 hover:text-red-400',
|
||||||
|
'dark:text-yellow-400 dark:hover:text-yellow-200')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{sp && <Separator />}
|
||||||
|
<Link to="/users/settings"
|
||||||
|
className={className}>
|
||||||
|
{user.name || '名もなきニジラー'}
|
||||||
|
</Link>
|
||||||
|
</>)
|
||||||
|
}
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
|
|
||||||
a
|
a
|
||||||
{
|
{
|
||||||
@apply text-blue-600 dark:text-blue-300;
|
@apply text-blue-700 dark:text-blue-300;
|
||||||
}
|
}
|
||||||
a:hover
|
a:hover
|
||||||
{
|
{
|
||||||
@apply text-blue-400 dark:text-blue-100;
|
@apply text-blue-500 dark:text-blue-100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする