|
- import axios from 'axios'
- import toCamel from 'camelcase-keys'
- import { useEffect, useRef, useState } from 'react'
- import { FaThumbsDown, FaThumbsUp } from 'react-icons/fa'
- import { useParams, useSearchParams } from 'react-router-dom'
-
- import ThreadCanvas from '@/components/threads/ThreadCanvas'
- import { API_BASE_URL } from '@/config'
-
- import type { ThreadCanvasHandle } from '@/components/threads/ThreadCanvas'
- import type { Post, Thread } from '@/types'
-
- const Sort = {
- Newest: 'newest',
- Oldest: 'oldest',
- Likes: 'likes',
- Dislikes: 'dislikes' } as const
- type Sort = (typeof Sort)[keyof typeof Sort]
-
-
- const reformPost = (post: Post): Post => ({
- ...post,
- createdAt: (new Date (post.createdAt)).toLocaleString ('ja-JP-u-ca-japanese') })
-
-
- export default () => {
- const { id } = useParams ()
-
- const canvasRef = useRef<ThreadCanvasHandle> (null)
-
- const [searchParams, setSearchParams] = useSearchParams ()
- const sort = searchParams.get ('sort') ?? 'created_at'
- const order = searchParams.get ('order') ?? 'desc'
- const sortKey = (() => {
- if (sort === 'score')
- {
- if (order === 'asc')
- return Sort.Dislikes
- return Sort.Likes
- }
- else
- {
- if (order === 'asc')
- return Sort.Oldest
- return Sort.Newest
- }
- }) ()
-
- const [loading, setLoading] = useState (true)
- const [message, setMessage] = useState ('')
- const [name, setName] = useState ('')
- const [password, setPassword] = useState ('')
- const [posts, setPosts] = useState<Post[]> ([])
- const [sending, setSending] = useState (false)
- const [thread, setThread] = useState<Thread | null> (null)
- const [withImage, setWithImage] = useState (false)
-
- const handleSend = async () => {
- if (!(message) && !(withImage))
- {
- alert ('メッセージ書くか画像描くかどっちかはしろ.')
- return
- }
-
- setSending (true)
- try
- {
- const form = new FormData
- if (withImage)
- {
- if (!(canvasRef.current))
- return
- const { dataURL } = await canvasRef.current.exportAll ()
- const blob = await (await fetch (dataURL)).blob ()
- form.append ('image', new File ([blob], `kekec_${ Date.now () }.png`, { type: 'image/png' }))
- }
- form.append ('name', name)
- form.append ('message', message)
- form.append ('password', password)
- const res = await axios.post (`${ API_BASE_URL }/threads/${ id }/posts`,
- form,
- { headers: { 'Content-Type': 'multipart/form-data' } })
- const data: Post = toCamel (res.data as any, { deep: true })
- setPosts(prev => [reformPost (data), ...prev])
-
- setMessage ('')
- if (withImage)
- {
- canvasRef.current!.clear ()
- setWithImage (false)
- }
-
- localStorage.setItem ('name', name)
- localStorage.setItem ('password', password)
- }
- finally
- {
- setSending (false)
- }
- }
-
- useEffect (() => {
- const nameRaw = localStorage.getItem ('name')
- nameRaw && setName (nameRaw)
-
- const passRaw = localStorage.getItem ('password')
- passRaw && setPassword (passRaw)
-
- const fetchThread = async () => {
- try
- {
- const res = await axios.get (`${ API_BASE_URL }/threads/${ id }`)
- setThread (toCamel (res.data as any, { deep: true }) as Thread)
- }
- catch
- {
- setThread (null)
- }
- }
- fetchThread ()
- }, [])
-
- useEffect (() => {
- const fetchPosts = async () => {
- try
- {
- const res = await axios.get (`${ API_BASE_URL }/threads/${ id }/posts`,
- { params: { sort, order } })
- const data = toCamel (res.data as any, { deep: true }) as Post[]
- setPosts (data.map (reformPost))
- }
- catch
- {
- setPosts ([])
- }
- setLoading (false)
- }
- fetchPosts ()
- }, [sortKey])
-
- useEffect (() => {
- if (!(thread))
- return
- document.title = `${ thread.name } - キケッツチャンネル お絵描き掲示板`
- }, [thread])
-
- return (
- <>
- <div className="mb-16">
- <div>
- <label>名前:</label>
- <input type="text"
- value={name}
- onChange={ev => setName (ev.target.value)} />
- </div>
- <div>
- <label>削除用パスワード:</label>
- <input type="password"
- value={password}
- onChange={ev => setPassword (ev.target.value)} />
- </div>
- <div>
- <label>メッセージ:</label>
- <textarea value={message}
- onChange={ev => setMessage (ev.target.value)} />
- </div>
- <div>
- <label>
- <input type="checkbox"
- checked={withImage}
- onChange={ev => setWithImage (ev.target.checked)} />
- イラストを投稿に含める
- </label>
- </div>
- <ThreadCanvas ref={canvasRef} visible={withImage} />
- <div>
- <button onClick={handleSend} disabled={sending}>
- {sending ? '送信中...' : '送信'}
- </button>
- </div>
- </div>
- {loading ? 'Loading...' : (
- posts.length > 0
- ? (
- <>
- <div>
- <label>
- 並べ替え:
- <select value={sortKey} onChange={ev => {
- switch (ev.target.value)
- {
- case Sort.Newest:
- searchParams.set ('sort', 'created_at')
- searchParams.set ('order', 'desc')
- break
- case Sort.Likes:
- searchParams.set ('sort', 'score')
- searchParams.set ('order', 'desc')
- break
- case Sort.Dislikes:
- searchParams.set ('sort', 'score')
- searchParams.set ('order', 'asc')
- break
- case Sort.Oldest:
- searchParams.set ('sort', 'created_at')
- searchParams.set ('order', 'asc')
- break
- }
- setSearchParams (searchParams)
- }}>
- <option value={Sort.Newest}>投稿が新しい順</option>
- <option value={Sort.Likes}>評価が高い順</option>
- <option value={Sort.Dislikes}>評価が低い順</option>
- <option value={Sort.Oldest}>投稿が古い順</option>
- </select>
- </label>
- </div>
- {posts.map (post => (
- <div key={post.id}
- id={post.postNo.toString ()}
- className="bg-white dark:bg-gray-800 p-3 m-4
- border border-gray-400 rounded-xl
- text-center">
- <div className="flex justify-between items-center px-2 py-1">
- <span>{post.postNo}: {post.name || '名なしさん'}</span>
- <span>{post.createdAt}</span>
- </div>
- <div className="flex items-center px-4 pt-1 pb-3">
- <a className="text-blue-600 dark:text-blue-300 mr-1 whitespace-nowrap"
- href="#"
- onClick={async ev => {
- ev.preventDefault ()
- try
- {
- await axios.post (`${ API_BASE_URL }/posts/${ post.id }/bad`)
- setPosts (prev => prev.map (p => (p.id === post.id
- ? { ...p, bad: p.bad + 1 }
- : p)))
- }
- catch
- {
- ;
- }
- }}>
- <FaThumbsDown className="inline" /> {post.bad}
- </a>
- <div className="h-2 bg-blue-600 dark:bg-blue-300"
- style={{ width: `${ post.good + post.bad === 0
- ? 50
- : post.bad * 100 / (post.good + post.bad) }%` }}>
- </div>
- <div className="h-2 bg-red-600 dark:bg-red-300"
- style={{ width: `${ post.good + post.bad === 0
- ? 50
- : post.good * 100 / (post.good + post.bad) }%` }}>
- </div>
- <a className="text-red-600 dark:text-red-300 ml-1 whitespace-nowrap"
- href="#"
- onClick={async ev => {
- ev.preventDefault ()
- try
- {
- await axios.post (`${ API_BASE_URL }/posts/${ post.id }/good`)
- setPosts (prev => prev.map (p => (p.id === post.id
- ? { ...p, good: p.good + 1 }
- : p)))
- }
- catch
- {
- ;
- }
- }}>
- {post.good} <FaThumbsUp className="inline" />
- </a>
- </div>
- {post.deletedAt
- ? <em className="font-bold">削除されました.</em>
- : (
- <>
- {post.message && (
- <div className="text-left px-4 my-2">
- {post.message}
- </div>)}
- {post.imageUrl && (
- <div className="bg-white inline-block border border-black dark:border-white">
- <img src={`${ API_BASE_URL }${ post.imageUrl }`} />
- </div>)}
- </>)}
- </div>))}
- </>)
- : 'レスないよ(笑).')}
- </>)
- }
|