コミットを比較
6 コミット
e5048dc9b3
...
250ee3f011
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| 250ee3f011 | |||
| 7d1a87f452 | |||
| a0d6aeb91e | |||
| be983e4ad1 | |||
| 82302cd3d1 | |||
| a01c63d972 |
@@ -1,6 +1,4 @@
|
|||||||
class NicoTagsController < ApplicationController
|
class NicoTagsController < ApplicationController
|
||||||
TAG_JSON = { only: [:id, :category, :post_count], methods: [:name, :has_wiki] }.freeze
|
|
||||||
|
|
||||||
def index
|
def index
|
||||||
limit = (params[:limit] || 20).to_i
|
limit = (params[:limit] || 20).to_i
|
||||||
cursor = params[:cursor].presence
|
cursor = params[:cursor].presence
|
||||||
@@ -19,8 +17,8 @@ class NicoTagsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
render json: { tags: tags.map { |tag|
|
render json: { tags: tags.map { |tag|
|
||||||
tag.as_json(TAG_JSON).merge(linked_tags: tag.linked_tags.map { |lt|
|
TagRepr.base(tag).merge(linked_tags: tag.linked_tags.map { |lt|
|
||||||
lt.as_json(TAG_JSON)
|
TagRepr.base(lt)
|
||||||
})
|
})
|
||||||
}, next_cursor: }
|
}, next_cursor: }
|
||||||
end
|
end
|
||||||
@@ -41,6 +39,6 @@ class NicoTagsController < ApplicationController
|
|||||||
tag.linked_tags = linked_tags
|
tag.linked_tags = linked_tags
|
||||||
tag.save!
|
tag.save!
|
||||||
|
|
||||||
render json: tag.linked_tags.map { |t| t.as_json(TAG_JSON) }, status: :ok
|
render json: tag.linked_tags.map { |t| TagRepr.base(t) }, status: :ok
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -36,8 +36,7 @@ class PostsController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
render json: { posts: posts.map { |post|
|
render json: { posts: posts.map { |post|
|
||||||
post.as_json(include: { tags: { only: [:id, :category, :post_count],
|
PostRepr.base(post).tap do |json|
|
||||||
methods: [:name, :has_wiki] } }).tap do |json|
|
|
||||||
json['thumbnail'] =
|
json['thumbnail'] =
|
||||||
if post.thumbnail.attached?
|
if post.thumbnail.attached?
|
||||||
rails_storage_proxy_url(post.thumbnail, only_path: false)
|
rails_storage_proxy_url(post.thumbnail, only_path: false)
|
||||||
@@ -60,10 +59,7 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
viewed = current_user&.viewed?(post) || false
|
viewed = current_user&.viewed?(post) || false
|
||||||
|
|
||||||
render json: (post
|
render json: PostRepr.base(post).merge(viewed:)
|
||||||
.as_json(include: { tags: { only: [:id, :category, :post_count],
|
|
||||||
methods: [:name, :has_wiki] } })
|
|
||||||
.merge(viewed:))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -102,9 +98,7 @@ class PostsController < ApplicationController
|
|||||||
sync_post_tags!(post, tags)
|
sync_post_tags!(post, tags)
|
||||||
|
|
||||||
post.reload
|
post.reload
|
||||||
render json: post.as_json(include: { tags: { only: [:id, :category, :post_count],
|
render json: PostRepr.base(post), status: :created
|
||||||
methods: [:name, :has_wiki] } }),
|
|
||||||
status: :created
|
|
||||||
else
|
else
|
||||||
render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
|
render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -170,7 +164,7 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
events = []
|
events = []
|
||||||
pts.each do |pt|
|
pts.each do |pt|
|
||||||
tag = pt.tag.as_json(only: [:id, :category], methods: [:name, :has_wiki])
|
tag = TagRepr.base(pt.tag)
|
||||||
post = pt.post
|
post = pt.post
|
||||||
|
|
||||||
events << Event.new(
|
events << Event.new(
|
||||||
@@ -269,8 +263,7 @@ class PostsController < ApplicationController
|
|||||||
return nil unless tag
|
return nil unless tag
|
||||||
|
|
||||||
if path.include?(tag_id)
|
if path.include?(tag_id)
|
||||||
return tag.as_json(only: [:id, :category, :post_count],
|
return TagRepr.base(tag).merge(children: [])
|
||||||
methods: [:name, :has_wiki]).merge(children: [])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if memo.key?(tag_id)
|
if memo.key?(tag_id)
|
||||||
@@ -282,8 +275,7 @@ class PostsController < ApplicationController
|
|||||||
|
|
||||||
children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
|
children = child_ids.filter_map { |cid| build_node.(cid, new_path) }
|
||||||
|
|
||||||
memo[tag_id] = tag.as_json(only: [:id, :category, :post_count],
|
memo[tag_id] = TagRepr.base(tag).merge(children:)
|
||||||
methods: [:name, :has_wiki]).merge(children:)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
root_ids.filter_map { |id| build_node.call(id, []) }
|
root_ids.filter_map { |id| build_node.call(id, []) }
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class TagsController < ApplicationController
|
|||||||
tags = tags.where(posts: { id: post_id })
|
tags = tags.where(posts: { id: post_id })
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: tags.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki])
|
render json: TagRepr.base(tags)
|
||||||
end
|
end
|
||||||
|
|
||||||
def autocomplete
|
def autocomplete
|
||||||
@@ -57,8 +57,7 @@ class TagsController < ApplicationController
|
|||||||
tags = tags.order(Arel.sql('post_count DESC, tag_names.name')).limit(20).to_a
|
tags = tags.order(Arel.sql('post_count DESC, tag_names.name')).limit(20).to_a
|
||||||
|
|
||||||
render json: tags.map { |tag|
|
render json: tags.map { |tag|
|
||||||
tag.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki])
|
TagRepr.base(tag).merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id])
|
||||||
.merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id])
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -67,7 +66,7 @@ class TagsController < ApplicationController
|
|||||||
.includes(:tag_name, tag_name: :wiki_page)
|
.includes(:tag_name, tag_name: :wiki_page)
|
||||||
.find_by(id: params[:id])
|
.find_by(id: params[:id])
|
||||||
if tag
|
if tag
|
||||||
render json: tag.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki])
|
render json: TagRepr.base(tag)
|
||||||
else
|
else
|
||||||
head :not_found
|
head :not_found
|
||||||
end
|
end
|
||||||
@@ -81,7 +80,7 @@ class TagsController < ApplicationController
|
|||||||
.includes(:tag_name, tag_name: :wiki_page)
|
.includes(:tag_name, tag_name: :wiki_page)
|
||||||
.find_by(tag_names: { name: })
|
.find_by(tag_names: { name: })
|
||||||
if tag
|
if tag
|
||||||
render json: tag.as_json(only: [:id, :category, :post_count], methods: [:name, :has_wiki])
|
render json: TagRepr.base(tag)
|
||||||
else
|
else
|
||||||
head :not_found
|
head :not_found
|
||||||
end
|
end
|
||||||
@@ -104,6 +103,6 @@ class TagsController < ApplicationController
|
|||||||
tag.update!(category:)
|
tag.update!(category:)
|
||||||
end
|
end
|
||||||
|
|
||||||
render json: tag.as_json(methods: [:name])
|
render json: TagRepr.base(tag)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ class WikiPagesController < ApplicationController
|
|||||||
def index
|
def index
|
||||||
title = params[:title].to_s.strip
|
title = params[:title].to_s.strip
|
||||||
if title.blank?
|
if title.blank?
|
||||||
return render json: WikiPage.joins(:tag_name)
|
return render json: WikiPageRepr.base(WikiPage.joins(:tag_name).includes(:tag_name))
|
||||||
.includes(:tag_name)
|
|
||||||
.as_json(methods: [:title])
|
|
||||||
end
|
end
|
||||||
|
|
||||||
q = WikiPage.joins(:tag_name).includes(:tag_name)
|
q = WikiPage.joins(:tag_name).includes(:tag_name)
|
||||||
.where('tag_names.name LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%")
|
.where('tag_names.name LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%")
|
||||||
render json: q.limit(20).as_json(methods: [:title])
|
render json: WikiPageRepr.base(q.limit(20))
|
||||||
end
|
end
|
||||||
|
|
||||||
def show
|
def show
|
||||||
@@ -98,7 +96,7 @@ class WikiPagesController < ApplicationController
|
|||||||
message = params[:message].presence
|
message = params[:message].presence
|
||||||
Wiki::Commit.content!(page:, body:, created_user: current_user, message:)
|
Wiki::Commit.content!(page:, body:, created_user: current_user, message:)
|
||||||
|
|
||||||
render json: page.as_json(methods: [:title]), status: :created
|
render json: WikiPageRepr.base(page), status: :created
|
||||||
else
|
else
|
||||||
render json: { errors: page.errors.full_messages },
|
render json: { errors: page.errors.full_messages },
|
||||||
status: :unprocessable_entity
|
status: :unprocessable_entity
|
||||||
@@ -174,8 +172,7 @@ class WikiPagesController < ApplicationController
|
|||||||
succ = page.succ_revision_id(revision_id)
|
succ = page.succ_revision_id(revision_id)
|
||||||
updated_at = rev.created_at
|
updated_at = rev.created_at
|
||||||
|
|
||||||
render json: page.as_json(methods: [:title])
|
render json: WikiPageRepr.base(page).merge(body:, revision_id:, pred:, succ:, updated_at:)
|
||||||
.merge(body:, revision_id:, pred:, succ:, updated_at:)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def find_revision page
|
def find_revision page
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
|
||||||
|
module WikiPageRepr
|
||||||
|
BASE = { methods: [:title] }.freeze
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def base wiki_page
|
||||||
|
wiki_page.as_json(BASE)
|
||||||
|
end
|
||||||
|
|
||||||
|
def many wiki_pages
|
||||||
|
wiki_pages.map { |p| base(p) }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
|
||||||
|
module PostRepr
|
||||||
|
BASE = { include: { tags: TagRepr::BASE } }.freeze
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def base post
|
||||||
|
post.as_json(BASE)
|
||||||
|
end
|
||||||
|
|
||||||
|
def many posts
|
||||||
|
posts.map { |p| base(p) }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
|
||||||
|
module TagRepr
|
||||||
|
BASE = { only: [:id, :category, :post_count], methods: [:name, :has_wiki] }.freeze
|
||||||
|
|
||||||
|
module_function
|
||||||
|
|
||||||
|
def base tag
|
||||||
|
tag.as_json(BASE)
|
||||||
|
end
|
||||||
|
|
||||||
|
def many tags
|
||||||
|
tags.map { |t| base(t) }
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -18,17 +18,35 @@ export default (({ post }: Props) => {
|
|||||||
{
|
{
|
||||||
case 'nicovideo.jp':
|
case 'nicovideo.jp':
|
||||||
{
|
{
|
||||||
const [videoId] = url.pathname.match (/(?<=\/watch\/)[a-zA-Z0-9]+?(?=\/|$)/)!
|
const mVideoId = url.pathname.match (/(?<=\/watch\/)[a-zA-Z0-9]+?(?=\/|$)/)
|
||||||
|
if (!(mVideoId))
|
||||||
|
break
|
||||||
|
|
||||||
|
const [videoId] = mVideoId
|
||||||
|
|
||||||
return <NicoViewer id={videoId} width={640} height={360}/>
|
return <NicoViewer id={videoId} width={640} height={360}/>
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'twitter.com':
|
case 'twitter.com':
|
||||||
case 'x.com':
|
case 'x.com':
|
||||||
const [userId] = url.pathname.match (/(?<=\/)[^\/]+?(?=\/|$|\?)/)!
|
{
|
||||||
const [statusId] = url.pathname.match (/(?<=\/status\/)\d+?(?=\/|$|\?)/)!
|
const mUserId = url.pathname.match (/(?<=\/)[^\/]+?(?=\/|$|\?)/)
|
||||||
return <TwitterEmbed userId={userId} statusId={statusId}/>
|
const mStatusId = url.pathname.match (/(?<=\/status\/)\d+?(?=\/|$|\?)/)
|
||||||
|
if (!(mUserId) || !(mStatusId))
|
||||||
|
break
|
||||||
|
|
||||||
|
const [userId] = mUserId
|
||||||
|
const [statusId] = mStatusId
|
||||||
|
|
||||||
|
return <TwitterEmbed userId={userId} statusId={statusId}/>
|
||||||
|
}
|
||||||
|
|
||||||
case 'youtube.com':
|
case 'youtube.com':
|
||||||
{
|
{
|
||||||
const videoId = url.searchParams.get ('v')!
|
const videoId = url.searchParams.get ('v')
|
||||||
|
if (!(videoId))
|
||||||
|
break
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<YoutubeEmbed videoId={videoId} opts={{ playerVars: {
|
<YoutubeEmbed videoId={videoId} opts={{ playerVars: {
|
||||||
playsinline: 1,
|
playsinline: 1,
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ const getTokenAt = (value: string, pos: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const replaceToken = (value: string, start: number, end: number, text: string) => (
|
const replaceToken = (value: string, start: number, end: number, text: string) =>
|
||||||
`${ value.slice (0, start) }${ text }${ value.slice (end) }`)
|
`${ value.slice (0, start) }${ text }${ value.slice (end) }`
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -38,16 +38,17 @@ export default (({ tags, setTags }: Props) => {
|
|||||||
const ref = useRef<HTMLTextAreaElement> (null)
|
const ref = useRef<HTMLTextAreaElement> (null)
|
||||||
|
|
||||||
const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 })
|
const [bounds, setBounds] = useState<{ start: number; end: number }> ({ start: 0, end: 0 })
|
||||||
|
const [focused, setFocused] = useState (false)
|
||||||
const [suggestions, setSuggestions] = useState<Tag[]> ([])
|
const [suggestions, setSuggestions] = useState<Tag[]> ([])
|
||||||
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
|
const [suggestionsVsbl, setSuggestionsVsbl] = useState (false)
|
||||||
|
|
||||||
const handleTagSelect = (tag: Tag) => {
|
const handleTagSelect = (tag: Tag) => {
|
||||||
setSuggestionsVsbl (false)
|
setSuggestionsVsbl (false)
|
||||||
const textarea = ref.current!
|
const textarea = ref.current!
|
||||||
const newValue = replaceToken (tags, bounds.start, bounds.end, tag.name)
|
const newValue = replaceToken (tags, bounds.start, bounds.end, tag.name + ' ')
|
||||||
setTags (newValue)
|
setTags (newValue)
|
||||||
requestAnimationFrame (async () => {
|
requestAnimationFrame (async () => {
|
||||||
const p = bounds.start + tag.name.length
|
const p = bounds.start + tag.name.length + 1
|
||||||
textarea.selectionStart = textarea.selectionEnd = p
|
textarea.selectionStart = textarea.selectionEnd = p
|
||||||
textarea.focus ()
|
textarea.focus ()
|
||||||
await recompute (p, newValue)
|
await recompute (p, newValue)
|
||||||
@@ -56,14 +57,21 @@ export default (({ tags, setTags }: Props) => {
|
|||||||
|
|
||||||
const recompute = async (pos: number, v: string = tags) => {
|
const recompute = async (pos: number, v: string = tags) => {
|
||||||
const { start, end, token } = getTokenAt (v, pos)
|
const { start, end, token } = getTokenAt (v, pos)
|
||||||
|
if (!(token.trim ()))
|
||||||
|
{
|
||||||
|
setSuggestionsVsbl (false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setBounds ({ start, end })
|
setBounds ({ start, end })
|
||||||
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: { q: token } })
|
|
||||||
|
const data = await apiGet<Tag[]> ('/tags/autocomplete', { params: { q: token, nico: '0' } })
|
||||||
setSuggestions (data.filter (t => t.postCount > 0))
|
setSuggestions (data.filter (t => t.postCount > 0))
|
||||||
setSuggestionsVsbl (suggestions.length > 0)
|
setSuggestionsVsbl (suggestions.length > 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="relative w-full">
|
||||||
<Label>タグ</Label>
|
<Label>タグ</Label>
|
||||||
<TextArea
|
<TextArea
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -72,11 +80,20 @@ export default (({ tags, setTags }: Props) => {
|
|||||||
onSelect={async (ev: SyntheticEvent<HTMLTextAreaElement>) => {
|
onSelect={async (ev: SyntheticEvent<HTMLTextAreaElement>) => {
|
||||||
const pos = (ev.target as HTMLTextAreaElement).selectionStart
|
const pos = (ev.target as HTMLTextAreaElement).selectionStart
|
||||||
await recompute (pos)
|
await recompute (pos)
|
||||||
|
}}
|
||||||
|
onFocus={() => {
|
||||||
|
setFocused (true)
|
||||||
|
}}
|
||||||
|
onBlur={() => {
|
||||||
|
setFocused (false)
|
||||||
|
setSuggestionsVsbl (false)
|
||||||
}}/>
|
}}/>
|
||||||
<TagSearchBox suggestions={suggestionsVsbl && suggestions.length
|
{focused && (
|
||||||
? suggestions
|
<TagSearchBox
|
||||||
: [] as Tag[]}
|
suggestions={suggestionsVsbl && suggestions.length > 0
|
||||||
activeIndex={-1}
|
? suggestions
|
||||||
onSelect={handleTagSelect}/>
|
: [] as Tag[]}
|
||||||
|
activeIndex={-1}
|
||||||
|
onSelect={handleTagSelect}/>)}
|
||||||
</div>)
|
</div>)
|
||||||
}) satisfies FC<Props>
|
}) satisfies FC<Props>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import DateTimeField from '@/components/common/DateTimeField'
|
import DateTimeField from '@/components/common/DateTimeField'
|
||||||
import Label from '@/components/common/Label'
|
import Label from '@/components/common/Label'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
|
||||||
@@ -16,34 +17,52 @@ export default (({ originalCreatedFrom,
|
|||||||
setOriginalCreatedBefore }: Props) => (
|
setOriginalCreatedBefore }: Props) => (
|
||||||
<div>
|
<div>
|
||||||
<Label>オリジナルの作成日時</Label>
|
<Label>オリジナルの作成日時</Label>
|
||||||
<div className="my-1">
|
<div className="my-1 flex">
|
||||||
<DateTimeField
|
<div className="w-80">
|
||||||
className="mr-2"
|
<DateTimeField
|
||||||
value={originalCreatedFrom ?? undefined}
|
className="mr-2"
|
||||||
onChange={setOriginalCreatedFrom}
|
value={originalCreatedFrom ?? undefined}
|
||||||
onBlur={ev => {
|
onChange={setOriginalCreatedFrom}
|
||||||
const v = ev.target.value
|
onBlur={ev => {
|
||||||
if (!(v))
|
const v = ev.target.value
|
||||||
return
|
if (!(v))
|
||||||
const d = new Date (v)
|
return
|
||||||
if (d.getSeconds () === 0)
|
|
||||||
{
|
const d = new Date (v)
|
||||||
if (d.getMinutes () === 0 && d.getHours () === 0)
|
if (d.getMinutes () === 0 && d.getHours () === 0)
|
||||||
d.setDate (d.getDate () + 1)
|
d.setDate (d.getDate () + 1)
|
||||||
else
|
else
|
||||||
d.setMinutes (d.getMinutes () + 1)
|
d.setMinutes (d.getMinutes () + 1)
|
||||||
}
|
setOriginalCreatedBefore (d.toISOString ())
|
||||||
else
|
}}/>
|
||||||
d.setSeconds (d.getSeconds () + 1)
|
以降
|
||||||
setOriginalCreatedBefore (d.toISOString ())
|
</div>
|
||||||
}}/>
|
<div>
|
||||||
以降
|
<Button
|
||||||
|
className="bg-gray-600 text-white rounded"
|
||||||
|
onClick={() => {
|
||||||
|
setOriginalCreatedFrom (null)
|
||||||
|
}}>
|
||||||
|
リセット
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="my-1">
|
<div className="my-1 flex">
|
||||||
<DateTimeField
|
<div className="w-80">
|
||||||
className="mr-2"
|
<DateTimeField
|
||||||
value={originalCreatedBefore ?? undefined}
|
className="mr-2"
|
||||||
onChange={setOriginalCreatedBefore}/>
|
value={originalCreatedBefore ?? undefined}
|
||||||
より前
|
onChange={setOriginalCreatedBefore}/>
|
||||||
|
より前
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="bg-gray-600 text-white rounded"
|
||||||
|
onClick={() => {
|
||||||
|
setOriginalCreatedBefore (null)
|
||||||
|
}}>
|
||||||
|
リセット
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>)) satisfies FC<Props>
|
</div>)) satisfies FC<Props>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { cn } from '@/lib/utils'
|
|||||||
import type { FC, FocusEvent } from 'react'
|
import type { FC, FocusEvent } from 'react'
|
||||||
|
|
||||||
|
|
||||||
const pad = (n: number) => n.toString ().padStart (2, '0')
|
const pad = (n: number): string => n.toString ().padStart (2, '0')
|
||||||
|
|
||||||
|
|
||||||
const toDateTimeLocalValue = (d: Date) => {
|
const toDateTimeLocalValue = (d: Date) => {
|
||||||
@@ -14,8 +14,7 @@ const toDateTimeLocalValue = (d: Date) => {
|
|||||||
const day = pad (d.getDate ())
|
const day = pad (d.getDate ())
|
||||||
const h = pad (d.getHours ())
|
const h = pad (d.getHours ())
|
||||||
const min = pad (d.getMinutes ())
|
const min = pad (d.getMinutes ())
|
||||||
const s = pad (d.getSeconds ())
|
return `${ y }-${ m }-${ day }T${ h }:${ min }:00`
|
||||||
return `${ y }-${ m }-${ day }T${ h }:${ min }:${ s }`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -37,7 +36,6 @@ export default (({ value, onChange, className, onBlur }: Props) => {
|
|||||||
<input
|
<input
|
||||||
className={cn ('border rounded p-2', className)}
|
className={cn ('border rounded p-2', className)}
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
step={1}
|
|
||||||
value={local}
|
value={local}
|
||||||
onChange={ev => {
|
onChange={ev => {
|
||||||
const v = ev.target.value
|
const v = ev.target.value
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { clsx, type ClassValue } from 'clsx'
|
import { clsx } from 'clsx'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
export function cn (...inputs: ClassValue[]) {
|
import type { ClassValue } from 'clsx'
|
||||||
return twMerge(clsx(...inputs))
|
|
||||||
}
|
|
||||||
|
export const cn = (...inputs: ClassValue[]) => twMerge (clsx (...inputs))
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import MainArea from '@/components/layout/MainArea'
|
|||||||
import { SITE_TITLE } from '@/config'
|
import { SITE_TITLE } from '@/config'
|
||||||
import { fetchPostChanges } from '@/lib/posts'
|
import { fetchPostChanges } from '@/lib/posts'
|
||||||
import { postsKeys } from '@/lib/queryKeys'
|
import { postsKeys } from '@/lib/queryKeys'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ export default (() => {
|
|||||||
{loading ? 'Loading...' : (
|
{loading ? 'Loading...' : (
|
||||||
<>
|
<>
|
||||||
<table className="table-auto w-full border-collapse">
|
<table className="table-auto w-full border-collapse">
|
||||||
<thead>
|
<thead className="border-b-2 border-black dark:border-white">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="p-2 text-left">投稿</th>
|
<th className="p-2 text-left">投稿</th>
|
||||||
<th className="p-2 text-left">変更</th>
|
<th className="p-2 text-left">変更</th>
|
||||||
@@ -64,9 +65,12 @@ export default (() => {
|
|||||||
++rowsCnt
|
++rowsCnt
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<tr key={`${ change.timestamp }-${ change.post.id }-${ change.tag.id }`}>
|
<tr key={`${ change.timestamp }-${ change.post.id }-${ change.tag.id }`}
|
||||||
|
className={cn ('even:bg-gray-100 dark:even:bg-gray-700',
|
||||||
|
withPost && 'border-t')}>
|
||||||
{withPost && (
|
{withPost && (
|
||||||
<td className="align-top" rowSpan={rowsCnt}>
|
<td className="align-top p-2 bg-white dark:bg-[#242424] border-r"
|
||||||
|
rowSpan={rowsCnt}>
|
||||||
<PrefetchLink to={`/posts/${ change.post.id }`}>
|
<PrefetchLink to={`/posts/${ change.post.id }`}>
|
||||||
<img src={change.post.thumbnail || change.post.thumbnailBase || undefined}
|
<img src={change.post.thumbnail || change.post.thumbnailBase || undefined}
|
||||||
alt={change.post.title || change.post.url}
|
alt={change.post.title || change.post.url}
|
||||||
@@ -74,11 +78,11 @@ export default (() => {
|
|||||||
className="w-40"/>
|
className="w-40"/>
|
||||||
</PrefetchLink>
|
</PrefetchLink>
|
||||||
</td>)}
|
</td>)}
|
||||||
<td>
|
<td className="p-2">
|
||||||
<TagLink tag={change.tag} withWiki={false} withCount={false}/>
|
<TagLink tag={change.tag} withWiki={false} withCount={false}/>
|
||||||
{`を${ change.changeType === 'add' ? '記載' : '消除' }`}
|
{`を${ change.changeType === 'add' ? '記載' : '消除' }`}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td className="p-2">
|
||||||
{change.user ? (
|
{change.user ? (
|
||||||
<PrefetchLink to={`/users/${ change.user.id }`}>
|
<PrefetchLink to={`/users/${ change.user.id }`}>
|
||||||
{change.user.name}
|
{change.user.name}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'
|
|||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
|
|
||||||
import TagLink from '@/components/TagLink'
|
import TagLink from '@/components/TagLink'
|
||||||
import SectionTitle from '@/components/common/SectionTitle'
|
import PageTitle from '@/components/common/PageTitle'
|
||||||
import TextArea from '@/components/common/TextArea'
|
import TextArea from '@/components/common/TextArea'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import { toast } from '@/components/ui/use-toast'
|
import { toast } from '@/components/ui/use-toast'
|
||||||
@@ -92,13 +92,13 @@ export default ({ user }: Props) => {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<div className="max-w-xl">
|
<div className="max-w-xl">
|
||||||
<SectionTitle>ニコニコ連携</SectionTitle>
|
<PageTitle>ニコニコ連携</PageTitle>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
{nicoTags.length > 0 && (
|
{nicoTags.length > 0 && (
|
||||||
<table className="table-auto w-full border-collapse mb-4">
|
<table className="table-auto w-full border-collapse mb-4">
|
||||||
<thead>
|
<thead className="border-b-2 border-black dark:border-white">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="p-2 text-left">ニコニコタグ</th>
|
<th className="p-2 text-left">ニコニコタグ</th>
|
||||||
<th className="p-2 text-left">連携タグ</th>
|
<th className="p-2 text-left">連携タグ</th>
|
||||||
@@ -107,7 +107,7 @@ export default ({ user }: Props) => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{nicoTags.map ((tag, i) => (
|
{nicoTags.map ((tag, i) => (
|
||||||
<tr key={i}>
|
<tr key={i} className="even:bg-gray-100 dark:even:bg-gray-700">
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
<TagLink tag={tag} withWiki={false} withCount={false}/>
|
<TagLink tag={tag} withWiki={false} withCount={false}/>
|
||||||
</td>
|
</td>
|
||||||
@@ -125,7 +125,7 @@ export default ({ user }: Props) => {
|
|||||||
</span>))}
|
</span>))}
|
||||||
</td>
|
</td>
|
||||||
{memberFlg && (
|
{memberFlg && (
|
||||||
<td>
|
<td className="p-2">
|
||||||
<a href="#" onClick={ev => {
|
<a href="#" onClick={ev => {
|
||||||
ev.preventDefault ()
|
ev.preventDefault ()
|
||||||
handleEdit (tag.id)
|
handleEdit (tag.id)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { Helmet } from 'react-helmet-async'
|
|||||||
import { useLocation } from 'react-router-dom'
|
import { useLocation } from 'react-router-dom'
|
||||||
|
|
||||||
import PrefetchLink from '@/components/PrefetchLink'
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
|
import PageTitle from '@/components/common/PageTitle'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import { SITE_TITLE } from '@/config'
|
import { SITE_TITLE } from '@/config'
|
||||||
import { apiGet } from '@/lib/api'
|
import { apiGet } from '@/lib/api'
|
||||||
@@ -28,8 +29,11 @@ export default () => {
|
|||||||
<Helmet>
|
<Helmet>
|
||||||
<title>{`Wiki 変更履歴 | ${ SITE_TITLE }`}</title>
|
<title>{`Wiki 変更履歴 | ${ SITE_TITLE }`}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
|
<PageTitle>Wiki 履歴</PageTitle>
|
||||||
|
|
||||||
<table className="table-auto w-full border-collapse">
|
<table className="table-auto w-full border-collapse">
|
||||||
<thead>
|
<thead className="border-b-2 border-black dark:border-white">
|
||||||
<tr>
|
<tr>
|
||||||
<th></th>
|
<th></th>
|
||||||
<th className="p-2 text-left">タイトル</th>
|
<th className="p-2 text-left">タイトル</th>
|
||||||
@@ -39,8 +43,8 @@ export default () => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{changes.map (change => (
|
{changes.map (change => (
|
||||||
<tr key={change.revisionId}>
|
<tr key={change.revisionId} className="even:bg-gray-100 dark:even:bg-gray-700">
|
||||||
<td>
|
<td className="p-2">
|
||||||
{change.pred != null && (
|
{change.pred != null && (
|
||||||
<PrefetchLink
|
<PrefetchLink
|
||||||
to={`/wiki/${ change.wikiPage.id }/diff?from=${ change.pred }&to=${ change.revisionId }`}>
|
to={`/wiki/${ change.wikiPage.id }/diff?from=${ change.pred }&to=${ change.revisionId }`}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
|
|||||||
import { Helmet } from 'react-helmet-async'
|
import { Helmet } from 'react-helmet-async'
|
||||||
|
|
||||||
import PrefetchLink from '@/components/PrefetchLink'
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import SectionTitle from '@/components/common/SectionTitle'
|
import PageTitle from '@/components/common/PageTitle'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import { SITE_TITLE } from '@/config'
|
import { SITE_TITLE } from '@/config'
|
||||||
import { apiGet } from '@/lib/api'
|
import { apiGet } from '@/lib/api'
|
||||||
@@ -35,8 +35,9 @@ export default () => {
|
|||||||
<Helmet>
|
<Helmet>
|
||||||
<title>Wiki | {SITE_TITLE}</title>
|
<title>Wiki | {SITE_TITLE}</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<div className="max-w-xl">
|
<div className="max-w-xl">
|
||||||
<SectionTitle>Wiki</SectionTitle>
|
<PageTitle>Wiki</PageTitle>
|
||||||
<form onSubmit={handleSearch} className="space-y-2">
|
<form onSubmit={handleSearch} className="space-y-2">
|
||||||
{/* タイトル */}
|
{/* タイトル */}
|
||||||
<div>
|
<div>
|
||||||
@@ -68,7 +69,7 @@ export default () => {
|
|||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<table className="table-auto w-full border-collapse">
|
<table className="table-auto w-full border-collapse">
|
||||||
<thead>
|
<thead className="border-b-2 border-black dark:border-white">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="p-2 text-left">タイトル</th>
|
<th className="p-2 text-left">タイトル</th>
|
||||||
<th className="p-2 text-left">最終更新</th>
|
<th className="p-2 text-left">最終更新</th>
|
||||||
@@ -76,13 +77,13 @@ export default () => {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{results.map (page => (
|
{results.map (page => (
|
||||||
<tr key={page.id}>
|
<tr key={page.id} className="even:bg-gray-100 dark:even:bg-gray-700">
|
||||||
<td className="p-2">
|
<td className="p-2">
|
||||||
<PrefetchLink to={`/wiki/${ encodeURIComponent (page.title) }`}>
|
<PrefetchLink to={`/wiki/${ encodeURIComponent (page.title) }`}>
|
||||||
{page.title}
|
{page.title}
|
||||||
</PrefetchLink>
|
</PrefetchLink>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-2 text-gray-100 text-sm">
|
<td className="p-2">
|
||||||
{page.updatedAt}
|
{page.updatedAt}
|
||||||
</td>
|
</td>
|
||||||
</tr>))}
|
</tr>))}
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする