This commit is contained in:
@@ -48,3 +48,4 @@ group :development, :test do
|
|||||||
end
|
end
|
||||||
|
|
||||||
gem 'bcrypt', '~> 3.1'
|
gem 'bcrypt', '~> 3.1'
|
||||||
|
gem 'rack-cors'
|
||||||
|
|||||||
@@ -187,6 +187,9 @@ GEM
|
|||||||
raabro (1.4.0)
|
raabro (1.4.0)
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rack (3.2.0)
|
rack (3.2.0)
|
||||||
|
rack-cors (3.0.0)
|
||||||
|
logger
|
||||||
|
rack (>= 3.0.14)
|
||||||
rack-session (2.1.1)
|
rack-session (2.1.1)
|
||||||
base64 (>= 0.1.0)
|
base64 (>= 0.1.0)
|
||||||
rack (>= 3.0.0)
|
rack (>= 3.0.0)
|
||||||
@@ -325,6 +328,7 @@ DEPENDENCIES
|
|||||||
kamal
|
kamal
|
||||||
mysql2 (~> 0.5)
|
mysql2 (~> 0.5)
|
||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
|
rack-cors
|
||||||
rails (~> 8.0.2)
|
rails (~> 8.0.2)
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
solid_cable
|
solid_cable
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
class PostsController < ApplicationController
|
class PostsController < ApplicationController
|
||||||
before_action :set_post, only: [:good, :bad, :destroy]
|
before_action :set_post, only: [:show, :good, :bad, :destroy]
|
||||||
|
|
||||||
|
def show
|
||||||
|
render json: @post.as_json.merge(image_url: (
|
||||||
|
if @post.image.attached?
|
||||||
|
Rails.application.routes.url_helpers.rails_blob_url(@post.image, only_path: true)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end))
|
||||||
|
end
|
||||||
|
|
||||||
# POST /posts/:id/good
|
# POST /posts/:id/good
|
||||||
def good
|
def good
|
||||||
|
|||||||
@@ -10,7 +10,14 @@ class ThreadPostsController < ApplicationController
|
|||||||
.select('posts.*, (good - bad) AS score')
|
.select('posts.*, (good - bad) AS score')
|
||||||
.order("#{ sort } #{ order }")
|
.order("#{ sort } #{ order }")
|
||||||
|
|
||||||
render json: posts
|
render json: posts.map { |post|
|
||||||
|
post.as_json.merge(image_url: (
|
||||||
|
if post.image.attached?
|
||||||
|
Rails.application.routes.url_helpers.rails_blob_url(post.image, only_path: true)
|
||||||
|
else
|
||||||
|
nil
|
||||||
|
end))
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /api/threads/:thread_id/posts
|
# POST /api/threads/:thread_id/posts
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
class ThreadsController < ApplicationController
|
class ThreadsController < ApplicationController
|
||||||
# GET /api/threads
|
# GET /api/threads
|
||||||
def index
|
def index
|
||||||
threads = Topic.order(updated_at: :desc)
|
threads = Topic.includes(:posts).order(updated_at: :desc)
|
||||||
render json: threads
|
render json: threads.map { |t|
|
||||||
|
t.as_json(methods: [:post_count])
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /api/threads/:id
|
# GET /api/threads/:id
|
||||||
@@ -15,7 +17,7 @@ class ThreadsController < ApplicationController
|
|||||||
def create
|
def create
|
||||||
thread = Topic.new(thread_params)
|
thread = Topic.new(thread_params)
|
||||||
if thread.save
|
if thread.save
|
||||||
render json: thread, status: :created
|
render json: thread.as_json, status: :created
|
||||||
else
|
else
|
||||||
render json: { errors: thread.errors.full_messages }, status: :unprocessable_entity
|
render json: { errors: thread.errors.full_messages }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
@@ -24,6 +26,6 @@ class ThreadsController < ApplicationController
|
|||||||
private
|
private
|
||||||
|
|
||||||
def thread_params
|
def thread_params
|
||||||
params.require(:thread).permit(:title, :description)
|
params.require(:thread).permit(:name, :description)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,4 +6,8 @@ class Topic < ApplicationRecord
|
|||||||
scope :active, -> { where deleted_at: nil }
|
scope :active, -> { where deleted_at: nil }
|
||||||
|
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
|
|
||||||
|
def post_count
|
||||||
|
posts.count
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,12 +5,12 @@
|
|||||||
|
|
||||||
# Read more: https://github.com/cyu/rack-cors
|
# Read more: https://github.com/cyu/rack-cors
|
||||||
|
|
||||||
# Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
Rails.application.config.middleware.insert_before 0, Rack::Cors do
|
||||||
# allow do
|
allow do
|
||||||
# origins "example.com"
|
origins 'http://bbs.kekec.wiki:5173', 'https://bbs.kekec.wiki'
|
||||||
#
|
|
||||||
# resource "*",
|
resource "*",
|
||||||
# headers: :any,
|
headers: :any,
|
||||||
# methods: [:get, :post, :put, :patch, :delete, :options, :head]
|
methods: [:get, :post, :put, :patch, :delete, :options, :head]
|
||||||
# end
|
end
|
||||||
# end
|
end
|
||||||
|
|||||||
@@ -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 bgmSrc from '@/assets/music.mp3'
|
||||||
import ThreadListPage from '@/pages/threads/ThreadListPage'
|
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',
|
const colours = ['bg-fuchsia-500 dark:bg-fuchsia-900',
|
||||||
'bg-lime-500 dark:bg-lime-900',
|
'bg-lime-500 dark:bg-lime-900',
|
||||||
@@ -18,11 +18,12 @@ const colours = ['bg-fuchsia-500 dark:bg-fuchsia-900',
|
|||||||
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
const [bgm] = useState (new Audio (bgmSrc))
|
||||||
const [colourIndex, setColourIndex] = useState (0)
|
const [colourIndex, setColourIndex] = useState (0)
|
||||||
|
const [mute, setMute] = useState (false)
|
||||||
const [playing, setPlaying] = useState (false)
|
const [playing, setPlaying] = useState (false)
|
||||||
|
|
||||||
useEffect (() => {
|
useEffect (() => {
|
||||||
const bgm = new Audio (bgmSrc)
|
|
||||||
bgm.loop = true
|
bgm.loop = true
|
||||||
|
|
||||||
const playBGM = async () => {
|
const playBGM = async () => {
|
||||||
@@ -60,24 +61,43 @@ export default () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<div className={cn ('w-screen h-screen',
|
<div className={cn ('w-screen min-h-screen',
|
||||||
colours[colourIndex],
|
colours[colourIndex],
|
||||||
'transition-colors duration-[3s] ease-linear')}>
|
'transition-colors duration-[3s] ease-linear')}>
|
||||||
<h1 className="text-center py-7">
|
<div className="mx-auto max-w-[960px]">
|
||||||
<Link to="/"
|
<header className="pt-6 mb-8">
|
||||||
className="text-7xl text-transparent whitespace-nowrap
|
<h1 className="text-center">
|
||||||
bg-[linear-gradient(90deg,#ff0000,#ff8800,#ffff00,#00ff00,#00ffff,#0000ff,#ff00ff,#ff0000)]
|
<Link to="/"
|
||||||
bg-clip-text [transform:skewX(-13.5deg)]
|
className="text-7xl text-transparent whitespace-nowrap
|
||||||
inline-block bg-[length:200%_100%] animate-rainbow-scroll drop-shadow-[0_0_6px_black]
|
bg-[linear-gradient(90deg,#ff0000,#ff8800,#ffff00,#00ff00,#00ffff,#0000ff,#ff00ff,#ff0000)]
|
||||||
font-serif hover:text-transparent dark:hover:text-transparent">
|
bg-clip-text [transform:skewX(-13.5deg)]
|
||||||
クソ掲示板
|
inline-block bg-[length:200%_100%] animate-rainbow-scroll drop-shadow-[0_0_6px_black]
|
||||||
</Link>
|
font-serif hover:text-transparent dark:hover:text-transparent">
|
||||||
</h1>
|
クソ掲示板
|
||||||
<Routes>
|
</Link>
|
||||||
<Route path="/" element={<Navigate to="/threads" replace />} />
|
</h1>
|
||||||
<Route path="/threads" element={<ThreadListPage />} />
|
<div className="text-center my-6">
|
||||||
{/* <Route path="/threads/:id" element={<ThreadDetailPage />} /> */}
|
{playing && (
|
||||||
</Routes>
|
<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>
|
</div>
|
||||||
</BrowserRouter>)
|
</BrowserRouter>)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,41 @@
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
import toCamel from 'camelcase-keys'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
import { API_BASE_URL } from '@/config'
|
import { API_BASE_URL } from '@/config'
|
||||||
|
|
||||||
|
import type { Thread } from '@/types'
|
||||||
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const [threadName, setThreadName] = useState ('')
|
const navigate = useNavigate ()
|
||||||
|
|
||||||
|
const [disabled, setDisabled] = useState (false)
|
||||||
const [threadDescription, setThreadDescription] = useState ('')
|
const [threadDescription, setThreadDescription] = useState ('')
|
||||||
|
const [threadName, setThreadName] = useState ('')
|
||||||
|
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
const formData = new FormData
|
if (!(threadName))
|
||||||
formData.append ('title', threadName)
|
{
|
||||||
formData.append ('description', threadDescription)
|
alert ('スレ名入れろよ.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try
|
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
|
catch
|
||||||
{
|
{
|
||||||
;
|
setDisabled (false)
|
||||||
}
|
}
|
||||||
|
setThreadName ('')
|
||||||
|
setThreadDescription ('')
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -44,6 +59,7 @@ export default () => {
|
|||||||
|
|
||||||
{/* 作成 */}
|
{/* 作成 */}
|
||||||
<button type="button"
|
<button type="button"
|
||||||
|
disabled={disabled}
|
||||||
onClick={submit}>
|
onClick={submit}>
|
||||||
スレッド作成
|
スレッド作成
|
||||||
</button>
|
</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 = 'キケッツチャンネル お絵描き掲示板'
|
export const SITE_NAME = 'キケッツチャンネル お絵描き掲示板'
|
||||||
|
|||||||
@@ -26,8 +26,6 @@
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
|
|
||||||
color-scheme: light dark;
|
color-scheme: light dark;
|
||||||
color: rgba(255, 255, 255, 0.87);
|
|
||||||
background-color: #242424;
|
|
||||||
|
|
||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
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,
|
AccordionItemHeading,
|
||||||
AccordionItemPanel } from 'react-accessible-accordion'
|
AccordionItemPanel } from 'react-accessible-accordion'
|
||||||
import { FaChevronDown, FaChevronUp } from 'react-icons/fa'
|
import { FaChevronDown, FaChevronUp } from 'react-icons/fa'
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
import ThreadNewForm from '@/components/threads/ThreadNewForm'
|
import ThreadNewForm from '@/components/threads/ThreadNewForm'
|
||||||
import { API_BASE_URL } from '@/config'
|
import { API_BASE_URL } from '@/config'
|
||||||
|
|
||||||
|
import type { Thread } from '@/types'
|
||||||
|
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
|
const [formOpen, setFormOpen] = useState (false)
|
||||||
const [loading, setLoading] = useState (true)
|
const [loading, setLoading] = useState (true)
|
||||||
const [threads, setThreads] = useState<Thread[]> ([])
|
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 (() => {
|
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 ()
|
fetchThreads ()
|
||||||
setLoading (false)
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Accordion allowZeroExpanded
|
<Accordion allowZeroExpanded
|
||||||
onClick={() => setFormOpen (!formOpen)}
|
onClick={() => setFormOpen (!formOpen)}
|
||||||
className="mb-4">
|
className="mb-4 mx-auto">
|
||||||
<AccordionItem>
|
<AccordionItem>
|
||||||
<AccordionItemHeading>
|
<AccordionItemHeading>
|
||||||
<AccordionItemButton className="flex items-center">
|
<AccordionItemButton className="flex items-center">
|
||||||
@@ -57,20 +64,26 @@ export default () => {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
{loading ? 'Loading...' : (
|
{loading ? 'Loading...' : (
|
||||||
threads.length
|
threads.length > 0
|
||||||
? threads.map (thread => (
|
? threads.map (thread => (
|
||||||
<div className="bg-white p-2 mb-2 border border-gray-400
|
<div className="bg-white dark:bg-gray-800 p-3 m-4
|
||||||
rounded-xl">
|
border border-gray-400 rounded-xl">
|
||||||
<div>
|
<div>
|
||||||
<Link to={`/threads/${ thread.id }`}>
|
<Link to={`/threads/${ thread.id }`}>
|
||||||
<h3>{thread.title}</h3>
|
<h3>{thread.name}</h3>
|
||||||
</Link>
|
</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>
|
||||||
<div className="flex justify-between text-sm text-gray-600">
|
<div className="grid grid-cols-3 justify-between text-sm
|
||||||
<span>{thread.postCount} レス</span>
|
text-gray-600 dark:text-gray-300">
|
||||||
<span>{thread.updatedAt} 更新</span>
|
<span className="text-left">{thread.postCount} レス</span>
|
||||||
<span>{thread.createdAt} 作成</span>
|
<span className="text-center">{thread.updatedAt} 更新</span>
|
||||||
|
<span className="text-right">{thread.createdAt} 作成</span>
|
||||||
</div>
|
</div>
|
||||||
</div>))
|
</div>))
|
||||||
: 'スレないよ(笑).')}
|
: 'スレないよ(笑).')}
|
||||||
|
|||||||
@@ -6,5 +6,5 @@ import path from 'path'
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
resolve: { alias: { '@': path.resolve (__dirname, './src') } },
|
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