This commit is contained in:
@@ -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;
|
||||
}
|
||||
+38
-18
@@ -4,7 +4,7 @@ import { BrowserRouter, Link, Routes, Route, Navigate } from 'react-router-dom'
|
||||
|
||||
import bgmSrc from '@/assets/music.mp3'
|
||||
import ThreadListPage from '@/pages/threads/ThreadListPage'
|
||||
// import ThreadDetailPage from '@/pages/threads/ThreadDetailPage'
|
||||
import ThreadDetailPage from '@/pages/threads/ThreadDetailPage'
|
||||
|
||||
const colours = ['bg-fuchsia-500 dark:bg-fuchsia-900',
|
||||
'bg-lime-500 dark:bg-lime-900',
|
||||
@@ -18,11 +18,12 @@ const colours = ['bg-fuchsia-500 dark:bg-fuchsia-900',
|
||||
|
||||
|
||||
export default () => {
|
||||
const [bgm] = useState (new Audio (bgmSrc))
|
||||
const [colourIndex, setColourIndex] = useState (0)
|
||||
const [mute, setMute] = useState (false)
|
||||
const [playing, setPlaying] = useState (false)
|
||||
|
||||
useEffect (() => {
|
||||
const bgm = new Audio (bgmSrc)
|
||||
bgm.loop = true
|
||||
|
||||
const playBGM = async () => {
|
||||
@@ -60,24 +61,43 @@ export default () => {
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<div className={cn ('w-screen h-screen',
|
||||
<div className={cn ('w-screen min-h-screen',
|
||||
colours[colourIndex],
|
||||
'transition-colors duration-[3s] ease-linear')}>
|
||||
<h1 className="text-center py-7">
|
||||
<Link to="/"
|
||||
className="text-7xl text-transparent whitespace-nowrap
|
||||
bg-[linear-gradient(90deg,#ff0000,#ff8800,#ffff00,#00ff00,#00ffff,#0000ff,#ff00ff,#ff0000)]
|
||||
bg-clip-text [transform:skewX(-13.5deg)]
|
||||
inline-block bg-[length:200%_100%] animate-rainbow-scroll drop-shadow-[0_0_6px_black]
|
||||
font-serif hover:text-transparent dark:hover:text-transparent">
|
||||
クソ掲示板
|
||||
</Link>
|
||||
</h1>
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/threads" replace />} />
|
||||
<Route path="/threads" element={<ThreadListPage />} />
|
||||
{/* <Route path="/threads/:id" element={<ThreadDetailPage />} /> */}
|
||||
</Routes>
|
||||
<div className="mx-auto max-w-[960px]">
|
||||
<header className="pt-6 mb-8">
|
||||
<h1 className="text-center">
|
||||
<Link to="/"
|
||||
className="text-7xl text-transparent whitespace-nowrap
|
||||
bg-[linear-gradient(90deg,#ff0000,#ff8800,#ffff00,#00ff00,#00ffff,#0000ff,#ff00ff,#ff0000)]
|
||||
bg-clip-text [transform:skewX(-13.5deg)]
|
||||
inline-block bg-[length:200%_100%] animate-rainbow-scroll drop-shadow-[0_0_6px_black]
|
||||
font-serif hover:text-transparent dark:hover:text-transparent">
|
||||
クソ掲示板
|
||||
</Link>
|
||||
</h1>
|
||||
<div className="text-center my-6">
|
||||
{playing && (
|
||||
<a href="#" onClick={ev => {
|
||||
ev.preventDefault ()
|
||||
setMute (bgm.muted = !(mute))
|
||||
}}>
|
||||
{mute ? 'やっぱり BGM が恋しい人用' : 'BGM がうるさい人用'}
|
||||
</a>)}
|
||||
</div>
|
||||
</header>
|
||||
<main className="mb-8">
|
||||
<Routes>
|
||||
<Route path="/" element={<Navigate to="/threads" replace />} />
|
||||
<Route path="/threads" element={<ThreadListPage />} />
|
||||
<Route path="/threads/:id" element={<ThreadDetailPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
<hr />
|
||||
<footer className="text-center mt-8 pb-12 text-base text-gray-500 dark:text-gray-300">
|
||||
© このペィジへの投稿は,すべて,パブリック・ドメインとします.
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</BrowserRouter>)
|
||||
}
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
import axios from 'axios'
|
||||
import toCamel from 'camelcase-keys'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { API_BASE_URL } from '@/config'
|
||||
|
||||
import type { Thread } from '@/types'
|
||||
|
||||
|
||||
export default () => {
|
||||
const [threadName, setThreadName] = useState ('')
|
||||
const navigate = useNavigate ()
|
||||
|
||||
const [disabled, setDisabled] = useState (false)
|
||||
const [threadDescription, setThreadDescription] = useState ('')
|
||||
const [threadName, setThreadName] = useState ('')
|
||||
|
||||
const submit = async () => {
|
||||
const formData = new FormData
|
||||
formData.append ('title', threadName)
|
||||
formData.append ('description', threadDescription)
|
||||
if (!(threadName))
|
||||
{
|
||||
alert ('スレ名入れろよ.')
|
||||
return
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await axios.post (`${ API_BASE_URL }/threads`, formData)
|
||||
setDisabled (true)
|
||||
const res = await axios.post (`${ API_BASE_URL }/threads`, { thread:
|
||||
{ name: threadName, description: threadDescription } })
|
||||
const { id } = toCamel (res.data as any, { deep: true }) as Thread
|
||||
navigate (`/threads/${ id }`)
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
setDisabled (false)
|
||||
}
|
||||
setThreadName ('')
|
||||
setThreadDescription ('')
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -44,6 +59,7 @@ export default () => {
|
||||
|
||||
{/* 作成 */}
|
||||
<button type="button"
|
||||
disabled={disabled}
|
||||
onClick={submit}>
|
||||
スレッド作成
|
||||
</button>
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const API_BASE_URL = 'http://localhost:3003'
|
||||
export const API_BASE_URL = 'https://api.bbs.kekec.wiki'
|
||||
export const SITE_NAME = 'キケッツチャンネル お絵描き掲示板'
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import axios from 'axios'
|
||||
import toCamel from 'camelcase-keys'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FaThumbsDown, FaThumbsUp } from 'react-icons/fa'
|
||||
import { useParams } from 'react-router-dom'
|
||||
|
||||
import { API_BASE_URL } from '@/config'
|
||||
|
||||
|
||||
export default () => {
|
||||
const { id } = useParams ()
|
||||
|
||||
const [loading, setLoading] = useState (true)
|
||||
const [posts, setPosts] = useState<Post[]> ([])
|
||||
|
||||
useEffect (() => {
|
||||
const fetchPosts = async () => {
|
||||
setLoading (true)
|
||||
try
|
||||
{
|
||||
const res = await axios.get (`${ API_BASE_URL }/threads/${ id }/posts`)
|
||||
const data = toCamel (res.data as any, { deep: true }) as Post[]
|
||||
setPosts (data.map (p => ({
|
||||
...p,
|
||||
createdAt: (new Date (p.createdAt)).toLocaleString ('ja-JP-u-ca-japanese') })))
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
}
|
||||
setLoading (false)
|
||||
}
|
||||
|
||||
setPosts ([])
|
||||
fetchPosts ()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading ? 'Loading...' : (
|
||||
posts.length > 0
|
||||
? posts.map (post => (
|
||||
<div 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>
|
||||
<div className="bg-white inline-block">
|
||||
<img src={`${ API_BASE_URL }${ post.imageUrl }`} />
|
||||
</div>
|
||||
</div>))
|
||||
: 'レスないよ(笑).')}
|
||||
</>)
|
||||
}
|
||||
@@ -8,40 +8,47 @@ import { Accordion,
|
||||
AccordionItemHeading,
|
||||
AccordionItemPanel } from 'react-accessible-accordion'
|
||||
import { FaChevronDown, FaChevronUp } from 'react-icons/fa'
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
import ThreadNewForm from '@/components/threads/ThreadNewForm'
|
||||
import { API_BASE_URL } from '@/config'
|
||||
|
||||
import type { Thread } from '@/types'
|
||||
|
||||
|
||||
export default () => {
|
||||
const [formOpen, setFormOpen] = useState (false)
|
||||
const [loading, setLoading] = useState (true)
|
||||
const [threads, setThreads] = useState<Thread[]> ([])
|
||||
const [formOpen, setFormOpen] = useState (false)
|
||||
|
||||
const fetchThreads = async () => {
|
||||
setLoading (true)
|
||||
try
|
||||
{
|
||||
const res = await axios.get (`${ API_BASE_URL }/threads`)
|
||||
const data = toCamel (res.data as any, { deep: true }) as Thread[]
|
||||
const threads = data.filter (t => t.id !== 2)
|
||||
setThreads (threads.map (t => ({
|
||||
...t,
|
||||
createdAt: (new Date (t.createdAt)).toLocaleString ('ja-JP-u-ca-japanese'),
|
||||
updatedAt: (new Date (t.updatedAt)).toLocaleString ('ja-JP-u-ca-japanese') })))
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
}
|
||||
setLoading (false)
|
||||
}
|
||||
|
||||
useEffect (() => {
|
||||
const fetchThreads = async () => {
|
||||
try
|
||||
{
|
||||
const res = await axios.get (`${ API_BASE_URL }/threads`)
|
||||
const data = toCamel (res.data as any, { deep: true }) as { threads: Thread[] }
|
||||
setThreads (data.threads)
|
||||
}
|
||||
catch
|
||||
{
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
setLoading (true)
|
||||
fetchThreads ()
|
||||
setLoading (false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Accordion allowZeroExpanded
|
||||
onClick={() => setFormOpen (!formOpen)}
|
||||
className="mb-4">
|
||||
className="mb-4 mx-auto">
|
||||
<AccordionItem>
|
||||
<AccordionItemHeading>
|
||||
<AccordionItemButton className="flex items-center">
|
||||
@@ -57,20 +64,26 @@ export default () => {
|
||||
|
||||
<div>
|
||||
{loading ? 'Loading...' : (
|
||||
threads.length
|
||||
threads.length > 0
|
||||
? threads.map (thread => (
|
||||
<div className="bg-white p-2 mb-2 border border-gray-400
|
||||
rounded-xl">
|
||||
<div className="bg-white dark:bg-gray-800 p-3 m-4
|
||||
border border-gray-400 rounded-xl">
|
||||
<div>
|
||||
<Link to={`/threads/${ thread.id }`}>
|
||||
<h3>{thread.title}</h3>
|
||||
<h3>{thread.name}</h3>
|
||||
</Link>
|
||||
<p>{thread.description}</p>
|
||||
<div className="my-2">
|
||||
{thread.description?.replaceAll ('\r\n', '\n')
|
||||
.replaceAll ('\r', '\n')
|
||||
.split ('\n')
|
||||
.map (l => <p>{l}</p>)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between text-sm text-gray-600">
|
||||
<span>{thread.postCount} レス</span>
|
||||
<span>{thread.updatedAt} 更新</span>
|
||||
<span>{thread.createdAt} 作成</span>
|
||||
<div className="grid grid-cols-3 justify-between text-sm
|
||||
text-gray-600 dark:text-gray-300">
|
||||
<span className="text-left">{thread.postCount} レス</span>
|
||||
<span className="text-center">{thread.updatedAt} 更新</span>
|
||||
<span className="text-right">{thread.createdAt} 作成</span>
|
||||
</div>
|
||||
</div>))
|
||||
: 'スレないよ(笑).')}
|
||||
|
||||
@@ -6,5 +6,5 @@ import path from 'path'
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: { alias: { '@': path.resolve (__dirname, './src') } },
|
||||
server: { host: true },
|
||||
server: { host: true, allowedHosts: ['miteruzo.com', 'bbs.kekec.wiki'] },
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user