@@ -69,8 +69,11 @@ class PostsController < ApplicationController | |||||
url = params[:url] | url = params[:url] | ||||
thumbnail = params[:thumbnail] | thumbnail = params[:thumbnail] | ||||
tag_names = params[:tags].to_s.split(' ') | tag_names = params[:tags].to_s.split(' ') | ||||
original_created_from = params[:original_created_from] | |||||
original_created_before = params[:original_created_before] | |||||
post = Post.new(title:, url:, thumbnail_base: '', uploaded_user: current_user) | |||||
post = Post.new(title:, url:, thumbnail_base: '', uploaded_user: current_user, | |||||
original_created_from:, original_created_before:) | |||||
post.thumbnail.attach(thumbnail) | post.thumbnail.attach(thumbnail) | ||||
if post.save | if post.save | ||||
post.resized_thumbnail! | post.resized_thumbnail! | ||||
@@ -103,10 +106,12 @@ class PostsController < ApplicationController | |||||
title = params[:title] | title = params[:title] | ||||
tag_names = params[:tags].to_s.split(' ') | tag_names = params[:tags].to_s.split(' ') | ||||
original_created_from = params[:original_created_from] | |||||
original_created_before = params[:original_created_before] | |||||
post = Post.find(params[:id].to_i) | post = Post.find(params[:id].to_i) | ||||
tags = post.tags.where(category: 'nico').to_a + Tag.normalise_tags(tag_names) | tags = post.tags.where(category: 'nico').to_a + Tag.normalise_tags(tag_names) | ||||
if post.update(title:, tags:) | |||||
if post.update(title:, tags:, original_created_from:, original_created_before:) | |||||
render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }), | render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }), | ||||
status: :ok | status: :ok | ||||
else | else | ||||
@@ -15,6 +15,8 @@ class Post < ApplicationRecord | |||||
foreign_key: :target_post_id | foreign_key: :target_post_id | ||||
has_one_attached :thumbnail | has_one_attached :thumbnail | ||||
validate :validate_original_created_range | |||||
def as_json options = { } | def as_json options = { } | ||||
super(options).merge({ thumbnail: thumbnail.attached? ? | super(options).merge({ thumbnail: thumbnail.attached? ? | ||||
Rails.application.routes.url_helpers.rails_blob_url( | Rails.application.routes.url_helpers.rails_blob_url( | ||||
@@ -49,4 +51,20 @@ class Post < ApplicationRecord | |||||
filename: 'resized_thumbnail.jpg', | filename: 'resized_thumbnail.jpg', | ||||
content_type: 'image/jpeg') | content_type: 'image/jpeg') | ||||
end | end | ||||
private | |||||
def validate_original_created_range | |||||
f = original_created_from | |||||
b = original_created_before | |||||
return if f.blank? || b.blank? | |||||
f = Time.zone.parse(f) if String === f | |||||
b = Time.zone.parse(b) if String === b | |||||
return if !(f) || !(b) | |||||
if f >= b | |||||
errors.add :original_created_before, 'オリジナルの作成日時の順番がをかしぃです.' | |||||
end | |||||
end | |||||
end | end |
@@ -3,6 +3,7 @@ import toCamel from 'camelcase-keys' | |||||
import { useState } from 'react' | import { useState } from 'react' | ||||
import PostFormTagsArea from '@/components/PostFormTagsArea' | import PostFormTagsArea from '@/components/PostFormTagsArea' | ||||
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 { Button } from '@/components/ui/button' | ||||
import { API_BASE_URL } from '@/config' | import { API_BASE_URL } from '@/config' | ||||
@@ -16,6 +17,10 @@ type Props = { post: Post | |||||
export default (({ post, onSave }: Props) => { | export default (({ post, onSave }: Props) => { | ||||
const [originalCreatedBefore, setOriginalCreatedBefore] = | |||||
useState<string | null> (post.originalCreatedBefore) | |||||
const [originalCreatedFrom, setOriginalCreatedFrom] = | |||||
useState<string | null> (post.originalCreatedFrom) | |||||
const [title, setTitle] = useState (post.title) | const [title, setTitle] = useState (post.title) | ||||
const [tags, setTags] = useState<string> (post.tags | const [tags, setTags] = useState<string> (post.tags | ||||
.filter (t => t.category !== 'nico') | .filter (t => t.category !== 'nico') | ||||
@@ -23,13 +28,19 @@ export default (({ post, onSave }: Props) => { | |||||
.join (' ')) | .join (' ')) | ||||
const handleSubmit = async () => { | const handleSubmit = async () => { | ||||
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, | |||||
original_created_from: originalCreatedFrom, | |||||
original_created_before: originalCreatedBefore }, | |||||
{ 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 = toCamel (res.data as any, { deep: true }) as Post | const data = toCamel (res.data as any, { deep: true }) as Post | ||||
onSave ({ ...post, | onSave ({ ...post, | ||||
title: data.title, | |||||
tags: data.tags } as Post) | |||||
title: data.title, | |||||
tags: data.tags, | |||||
originalCreatedFrom: data.originalCreatedFrom, | |||||
originalCreatedBefore: data.originalCreatedBefore } as Post) | |||||
} | } | ||||
return ( | return ( | ||||
@@ -40,12 +51,29 @@ export default (({ post, onSave }: Props) => { | |||||
<input type="text" | <input type="text" | ||||
className="w-full border rounded p-2" | className="w-full border rounded p-2" | ||||
value={title} | value={title} | ||||
onChange={e => setTitle (e.target.value)}/> | |||||
onChange={ev => setTitle (ev.target.value)}/> | |||||
</div> | </div> | ||||
{/* タグ */} | {/* タグ */} | ||||
<PostFormTagsArea tags={tags} setTags={setTags}/> | <PostFormTagsArea tags={tags} setTags={setTags}/> | ||||
{/* オリジナルの作成日時 */} | |||||
<div> | |||||
<Label>オリジナルの作成日時</Label> | |||||
<div className="my-1"> | |||||
<DateTimeField | |||||
value={originalCreatedFrom ?? undefined} | |||||
onChange={setOriginalCreatedFrom}/> | |||||
以降 | |||||
</div> | |||||
<div className="my-1"> | |||||
<DateTimeField | |||||
value={originalCreatedBefore ?? undefined} | |||||
onChange={setOriginalCreatedBefore}/> | |||||
より前 | |||||
</div> | |||||
</div> | |||||
{/* 送信 */} | {/* 送信 */} | ||||
<Button onClick={handleSubmit} | <Button onClick={handleSubmit} | ||||
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"> | className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400"> | ||||
@@ -0,0 +1,43 @@ | |||||
import { useEffect, useState } from 'react' | |||||
import type { FC } from 'react' | |||||
const pad = (n: number) => n.toString ().padStart (2, '0') | |||||
const toDateTimeLocalValue = (d: Date) => { | |||||
const y = d.getFullYear () | |||||
const m = pad (d.getMonth () + 1) | |||||
const day = pad (d.getDate ()) | |||||
const h = pad (d.getHours ()) | |||||
const min = pad (d.getMinutes ()) | |||||
const s = pad (d.getSeconds ()) | |||||
return `${ y }-${ m }-${ day }T${ h }:${ min }:${ s }` | |||||
} | |||||
type Props = { | |||||
value?: string | |||||
onChange?: (isoUTC: string | null) => void } | |||||
export default (({ value, onChange }: Props) => { | |||||
const [local, setLocal] = useState ('') | |||||
useEffect (() => { | |||||
setLocal (value ? toDateTimeLocalValue (new Date (value)) : '') | |||||
}, [value]) | |||||
return ( | |||||
<input | |||||
className="border rounded p-2" | |||||
type="datetime-local" | |||||
step={1} | |||||
value={local} | |||||
onChange={ev => { | |||||
const v = ev.target.value | |||||
setLocal (v) | |||||
onChange?.(v ? (new Date (v)).toISOString () : null) | |||||
}}/>) | |||||
}) satisfies FC<Props> |
@@ -4,6 +4,7 @@ import { Helmet } from 'react-helmet-async' | |||||
import { useNavigate } from 'react-router-dom' | import { useNavigate } from 'react-router-dom' | ||||
import PostFormTagsArea from '@/components/PostFormTagsArea' | import PostFormTagsArea from '@/components/PostFormTagsArea' | ||||
import DateTimeField from '@/components/common/DateTimeField' | |||||
import Form from '@/components/common/Form' | import Form from '@/components/common/Form' | ||||
import Label from '@/components/common/Label' | import Label from '@/components/common/Label' | ||||
import PageTitle from '@/components/common/PageTitle' | import PageTitle from '@/components/common/PageTitle' | ||||
@@ -26,15 +27,17 @@ export default (({ user }: Props) => { | |||||
const navigate = useNavigate () | const navigate = useNavigate () | ||||
const [originalCreatedBefore, setOriginalCreatedBefore] = useState<string | null> (null) | |||||
const [originalCreatedFrom, setOriginalCreatedFrom] = useState<string | null> (null) | |||||
const [tags, setTags] = useState ('') | |||||
const [thumbnailAutoFlg, setThumbnailAutoFlg] = useState (true) | |||||
const [thumbnailFile, setThumbnailFile] = useState<File | null> (null) | |||||
const [thumbnailLoading, setThumbnailLoading] = useState (false) | |||||
const [thumbnailPreview, setThumbnailPreview] = useState<string> ('') | |||||
const [title, setTitle] = useState ('') | const [title, setTitle] = useState ('') | ||||
const [titleAutoFlg, setTitleAutoFlg] = useState (true) | const [titleAutoFlg, setTitleAutoFlg] = useState (true) | ||||
const [titleLoading, setTitleLoading] = useState (false) | const [titleLoading, setTitleLoading] = useState (false) | ||||
const [url, setURL] = useState ('') | const [url, setURL] = useState ('') | ||||
const [thumbnailFile, setThumbnailFile] = useState<File | null> (null) | |||||
const [thumbnailPreview, setThumbnailPreview] = useState<string> ('') | |||||
const [thumbnailAutoFlg, setThumbnailAutoFlg] = useState (true) | |||||
const [thumbnailLoading, setThumbnailLoading] = useState (false) | |||||
const [tags, setTags] = useState ('') | |||||
const previousURLRef = useRef ('') | const previousURLRef = useRef ('') | ||||
@@ -45,6 +48,10 @@ export default (({ user }: Props) => { | |||||
formData.append ('tags', tags) | formData.append ('tags', tags) | ||||
if (thumbnailFile) | if (thumbnailFile) | ||||
formData.append ('thumbnail', thumbnailFile) | formData.append ('thumbnail', thumbnailFile) | ||||
if (originalCreatedFrom) | |||||
formData.append ('original_created_from', originalCreatedFrom) | |||||
if (originalCreatedBefore) | |||||
formData.append ('original_created_before', originalCreatedBefore) | |||||
try | try | ||||
{ | { | ||||
@@ -122,7 +129,7 @@ export default (({ user }: Props) => { | |||||
{/* URL */} | {/* URL */} | ||||
<div> | <div> | ||||
<Label>URL</Label> | <Label>URL</Label> | ||||
<input type="text" | |||||
<input type="url" | |||||
placeholder="例:https://www.nicovideo.jp/watch/..." | placeholder="例:https://www.nicovideo.jp/watch/..." | ||||
value={url} | value={url} | ||||
onChange={e => setURL (e.target.value)} | onChange={e => setURL (e.target.value)} | ||||
@@ -181,6 +188,23 @@ export default (({ user }: Props) => { | |||||
{/* タグ */} | {/* タグ */} | ||||
<PostFormTagsArea tags={tags} setTags={setTags}/> | <PostFormTagsArea tags={tags} setTags={setTags}/> | ||||
{/* オリジナルの作成日時 */} | |||||
<div> | |||||
<Label>オリジナルの作成日時</Label> | |||||
<div className="my-1"> | |||||
<DateTimeField | |||||
value={originalCreatedFrom ?? undefined} | |||||
onChange={setOriginalCreatedFrom}/> | |||||
以降 | |||||
</div> | |||||
<div className="my-1"> | |||||
<DateTimeField | |||||
value={originalCreatedBefore ?? undefined} | |||||
onChange={setOriginalCreatedBefore}/> | |||||
より前 | |||||
</div> | |||||
</div> | |||||
{/* 送信 */} | {/* 送信 */} | ||||
<Button onClick={handleSubmit} | <Button onClick={handleSubmit} | ||||
className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400" | className="px-4 py-2 bg-blue-600 text-white rounded disabled:bg-gray-400" | ||||
@@ -17,14 +17,17 @@ export type NicoTag = Tag & { | |||||
linkedTags: Tag[] } | linkedTags: Tag[] } | ||||
export type Post = { | export type Post = { | ||||
id: number | |||||
url: string | |||||
title: string | |||||
thumbnail: string | |||||
thumbnailBase: string | |||||
tags: Tag[] | |||||
viewed: boolean | |||||
related: Post[] } | |||||
id: number | |||||
url: string | |||||
title: string | |||||
thumbnail: string | |||||
thumbnailBase: string | |||||
tags: Tag[] | |||||
viewed: boolean | |||||
related: Post[] | |||||
createdAt: string | |||||
originalCreatedFrom: string | null | |||||
originalCreatedBefore: string | null } | |||||
export type SubMenuItem = { | export type SubMenuItem = { | ||||
component: React.ReactNode | component: React.ReactNode | ||||