@@ -18,13 +18,13 @@ class PostsController < ApplicationController
|
|||||||
else
|
else
|
||||||
@posts = Post.all
|
@posts = Post.all
|
||||||
end
|
end
|
||||||
render json: @posts
|
render json: @posts.as_json(include: { tags: { only: [:id, :name, :category] } })
|
||||||
end
|
end
|
||||||
|
|
||||||
# GET /posts/1
|
# GET /posts/1
|
||||||
def show
|
def show
|
||||||
@post = Post.includes(:tags).find(params[:id])
|
@post = Post.includes(:tags).find(params[:id])
|
||||||
render json: @post.as_json(include: { tags: { only: [:id, :name] } })
|
render json: @post.as_json(include: { tags: { only: [:id, :name, :category] } })
|
||||||
end
|
end
|
||||||
|
|
||||||
# POST /posts
|
# POST /posts
|
||||||
|
|||||||
+33
-20
@@ -1,4 +1,4 @@
|
|||||||
import React from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
|
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'
|
||||||
import HomePage from './pages/HomePage'
|
import HomePage from './pages/HomePage'
|
||||||
import TagPage from './pages/TagPage'
|
import TagPage from './pages/TagPage'
|
||||||
@@ -7,28 +7,41 @@ import TagSidebar from './components/TagSidebar'
|
|||||||
import PostPage from './pages/PostPage'
|
import PostPage from './pages/PostPage'
|
||||||
import PostDetailPage from './pages/PostDetailPage'
|
import PostDetailPage from './pages/PostDetailPage'
|
||||||
|
|
||||||
|
type Tag = { id: number
|
||||||
|
name: string
|
||||||
|
category: string }
|
||||||
|
|
||||||
|
type Post = { id: number
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
thumbnail: string
|
||||||
|
tags: Tag[] }
|
||||||
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
|
const [posts, setPosts] = useState<Post[]> ([])
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
alert ('このサイトはまだ作りかけです!!!!\n出てけ!!!!!!!!!!!!!!!!!!!!')
|
||||||
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<div className="flex flex-col h-screen w-screen">
|
<div className="flex flex-col h-screen w-screen">
|
||||||
<TopNav />
|
<TopNav />
|
||||||
<div className="flex flex-1">
|
<div className="flex flex-1">
|
||||||
<Routes>
|
<TagSidebar posts={posts} setPosts={setPosts} />
|
||||||
<Route path="/posts/:postId" element={<TagSidebar />} />
|
<main className="flex-1 overflow-y-auto p-4">
|
||||||
<Route path="*" element={<TagSidebar />} />
|
<Routes>
|
||||||
</Routes>
|
<Route path="/posts/:id" element={<PostDetailPage posts={posts} setPosts={setPosts} />} />
|
||||||
<main className="flex-1 overflow-y-auto p-4">
|
<Route path="/tags/:tag" element={<TagPage />} />
|
||||||
<Routes>
|
<Route path="*" element={<PostPage posts={posts} setPosts={setPosts} />} />
|
||||||
<Route path="/" element={<PostPage />} />
|
</Routes>
|
||||||
<Route path="/posts" element={<PostPage />} />
|
</main>
|
||||||
<Route path="/posts/:id" element={<PostDetailPage />} />
|
</div>
|
||||||
<Route path="/tags/:tag" element={<TagPage />} />
|
|
||||||
</Routes>
|
|
||||||
</main>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Router>)
|
||||||
</Router>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default App
|
export default App
|
||||||
|
|||||||
@@ -15,23 +15,23 @@ const NicoViewer: React.FC = (props: Props) => {
|
|||||||
const iframeRef = useRef<HTMLIFrameElement> (null)
|
const iframeRef = useRef<HTMLIFrameElement> (null)
|
||||||
const [screenWidth, setScreenWidth] = useState<CSSProperties['width']> ()
|
const [screenWidth, setScreenWidth] = useState<CSSProperties['width']> ()
|
||||||
const [screenHeight, setScreenHeight] = useState<CSSProperties['height']> ()
|
const [screenHeight, setScreenHeight] = useState<CSSProperties['height']> ()
|
||||||
const [isLandscape, setIsLandScape] = useState<boolean> (false)
|
const [landscape, setLandscape] = useState<boolean> (false)
|
||||||
const [isFullScreen, setIsFullScreen] = useState<boolean> (false)
|
const [fullScreen, setFullScreen] = useState<boolean> (false)
|
||||||
|
|
||||||
const src = `https://embed.nicovideo.jp/watch/${id}?persistence=1&oldScript=1&referer=&from=0&allowProgrammaticFullScreen=1`;
|
const src = `https://embed.nicovideo.jp/watch/${id}?persistence=1&oldScript=1&referer=&from=0&allowProgrammaticFullScreen=1`;
|
||||||
|
|
||||||
const styleFullScreen: CSSProperties = isFullScreen ? {
|
const styleFullScreen: CSSProperties = fullScreen ? {
|
||||||
top: 0,
|
top: 0,
|
||||||
left: isLandscape ? 0 : '100%',
|
left: landscape ? 0 : '100%',
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
width: screenWidth,
|
width: screenWidth,
|
||||||
height: screenHeight,
|
height: screenHeight,
|
||||||
zIndex: 2147483647,
|
zIndex: 2147483647,
|
||||||
maxWidth: 'none',
|
maxWidth: 'none',
|
||||||
transformOrigin: '0% 0%',
|
transformOrigin: '0% 0%',
|
||||||
transform: isLandscape ? 'none' : 'rotate(90deg)',
|
transform: landscape ? 'none' : 'rotate(90deg)',
|
||||||
WebkitTransformOrigin: '0% 0%',
|
WebkitTransformOrigin: '0% 0%',
|
||||||
WebkitTransform: isLandscape ? 'none' : 'rotate(90deg)',
|
WebkitTransform: landscape ? 'none' : 'rotate(90deg)',
|
||||||
} : {};
|
} : {};
|
||||||
|
|
||||||
const margedStyle = {
|
const margedStyle = {
|
||||||
@@ -45,38 +45,40 @@ const NicoViewer: React.FC = (props: Props) => {
|
|||||||
const onMessage = (event: MessageEvent<any>) => {
|
const onMessage = (event: MessageEvent<any>) => {
|
||||||
if (!iframeRef.current || event.source !== iframeRef.current.contentWindow) return;
|
if (!iframeRef.current || event.source !== iframeRef.current.contentWindow) return;
|
||||||
if (event.data.eventName === 'enterProgrammaticFullScreen') {
|
if (event.data.eventName === 'enterProgrammaticFullScreen') {
|
||||||
setIsFullScreen(true);
|
setFullScreen(true);
|
||||||
} else if (event.data.eventName === 'exitProgrammaticFullScreen') {
|
} else if (event.data.eventName === 'exitProgrammaticFullScreen') {
|
||||||
setIsFullScreen(false);
|
setFullScreen(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('message', onMessage);
|
addEventListener('message', onMessage);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('message', onMessage);
|
removeEventListener('message', onMessage);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!isFullScreen) return;
|
if (!(fullScreen))
|
||||||
|
return
|
||||||
|
|
||||||
const initialScrollX = window.scrollX;
|
const initialScrollX = window.scrollX
|
||||||
const initialScrollY = window.scrollY;
|
const initialScrollY = window.scrollY
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout
|
||||||
let ended = false;
|
let ended = false
|
||||||
|
|
||||||
const pollingResize = () => {
|
const pollingResize = () => {
|
||||||
if (ended) return;
|
if (ended)
|
||||||
|
return
|
||||||
|
|
||||||
const isLandscape = window.innerWidth >= window.innerHeight;
|
const landscape = window.innerWidth >= window.innerHeight
|
||||||
const windowWidth = `${isLandscape ? window.innerWidth : window.innerHeight}px`;
|
const windowWidth = `${landscape ? window.innerWidth : window.innerHeight}px`
|
||||||
const windowHeight = `${isLandscape ? window.innerHeight : window.innerWidth}px`;
|
const windowHeight = `${landscape ? window.innerHeight : window.innerWidth}px`
|
||||||
|
|
||||||
setIsLandScape(isLandscape);
|
setLandScape (Landscape)
|
||||||
setScreenWidth(windowWidth);
|
setScreenWidth (windowWidth)
|
||||||
setScreenHeight(windowHeight);
|
setScreenHeight (windowHeight)
|
||||||
timer = setTimeout(startPollingResize, 200);
|
timer = setTimeout (startPollingResize, 200)
|
||||||
}
|
}
|
||||||
|
|
||||||
const startPollingResize = () => {
|
const startPollingResize = () => {
|
||||||
@@ -94,12 +96,13 @@ const NicoViewer: React.FC = (props: Props) => {
|
|||||||
ended = true;
|
ended = true;
|
||||||
window.scrollTo(initialScrollX, initialScrollY);
|
window.scrollTo(initialScrollX, initialScrollY);
|
||||||
};
|
};
|
||||||
}, [isFullScreen]);
|
}, [fullScreen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isFullScreen) return;
|
if (!(fullScreen))
|
||||||
window.scrollTo(0, 0);
|
return
|
||||||
}, [screenWidth, screenHeight, isFullScreen]);
|
scrollTo (0, 0)
|
||||||
|
}, [screenWidth, screenHeight, fullScreen])
|
||||||
|
|
||||||
return <iframe ref={iframeRef}
|
return <iframe ref={iframeRef}
|
||||||
src={src}
|
src={src}
|
||||||
|
|||||||
@@ -7,10 +7,8 @@ import { API_BASE_URL } from '../config'
|
|||||||
const TagSearch: React.FC = () => {
|
const TagSearch: React.FC = () => {
|
||||||
const navigate = useNavigate ()
|
const navigate = useNavigate ()
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
const query = new URLSearchParams (location.search)
|
|
||||||
const tagsQuery = query.get ('tags') ?? ''
|
|
||||||
|
|
||||||
const [search, setSearch] = useState (tagsQuery)
|
const [search, setSearch] = useState ('')
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === 'Enter' && search.length > 0)
|
if (e.key === 'Enter' && search.length > 0)
|
||||||
@@ -18,6 +16,12 @@ const TagSearch: React.FC = () => {
|
|||||||
{ replace: true })
|
{ replace: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
|
const query = new URLSearchParams (location.search)
|
||||||
|
const tagsQuery = query.get ('tags') ?? ''
|
||||||
|
setSearch (tagsQuery)
|
||||||
|
}, [location.search])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|||||||
@@ -4,65 +4,86 @@ import { Link, useParams } from 'react-router-dom'
|
|||||||
import { API_BASE_URL } from '../config'
|
import { API_BASE_URL } from '../config'
|
||||||
import TagSearch from './TagSearch'
|
import TagSearch from './TagSearch'
|
||||||
|
|
||||||
type Tag = { id: number
|
type Tag = { id: number
|
||||||
name: string }
|
name: string
|
||||||
|
category: string }
|
||||||
|
|
||||||
type TagByCategory = { [key: string]: Tag[] }
|
type TagByCategory = { [key: string]: Tag[] }
|
||||||
type OriginalTag = { id: number
|
|
||||||
name: string
|
type OriginalTag = { id: number
|
||||||
category: string }
|
name: string
|
||||||
|
category: string }
|
||||||
|
|
||||||
|
type Post = { id: number
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
thumbnail: string
|
||||||
|
tags: Tag[] }
|
||||||
|
|
||||||
|
type Props = { posts: Post[]
|
||||||
|
setPosts: (posts: Post[]) => void }
|
||||||
|
|
||||||
|
|
||||||
const tagNameMap: { [key: string]: string } = {
|
const tagNameMap: { [key: string]: string } = {
|
||||||
general: '一般',
|
general: '一般',
|
||||||
deerjikist: 'ニジラー',
|
deerjikist: 'ニジラー',
|
||||||
nico: 'ニコニコタグ' }
|
nico: 'ニコニコタグ' }
|
||||||
|
|
||||||
const TagSidebar: React.FC = () => {
|
const TagSidebar: React.FC = (props: Props) => {
|
||||||
const { postId } = useParams<{ postId?: number }> ()
|
const { posts, setPosts } = props
|
||||||
const [tags, setTags] = useState<TagByCategory> ({ })
|
|
||||||
|
|
||||||
useEffect(() => {
|
const [tags, setTags] = useState<TagByCategory> ({ })
|
||||||
|
const [tagsCounts, setTagsCounts] = useState<{ [key: id]: number }> ({ })
|
||||||
|
|
||||||
|
useEffect (() => {
|
||||||
const fetchTags = async () => {
|
const fetchTags = async () => {
|
||||||
try {
|
try
|
||||||
const response = await axios.get (`${API_BASE_URL}/tags`
|
{
|
||||||
+ (postId ? `?post=${ postId }` : ''))
|
let tagsTmp: TagByCategory = { }
|
||||||
const tagsTmp: TagByCategory = { }
|
let tagsCountsTmp: { [key: id]: number } = { }
|
||||||
for (const tag of response.data)
|
for (const post of posts)
|
||||||
{
|
{
|
||||||
if (!(tag.category in tagsTmp))
|
for (const tag of post.tags)
|
||||||
tagsTmp[tag.category] = []
|
{
|
||||||
tagsTmp[tag.category].push ({ id: tag.id, name: tag.name })
|
if (!(tag.category in tagsTmp))
|
||||||
|
tagsTmp[tag.category] = []
|
||||||
|
tagsTmp[tag.category].push (tag)
|
||||||
|
if (!(tag.id in tagsCountsTmp))
|
||||||
|
tagsCountsTmp[tag.id] = 0
|
||||||
|
++tagsCountsTmp[tag.id]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
for (const cat of Object.keys (tagsTmp))
|
||||||
|
tagsTmp[cat].sort ((tagA, tagB) => tagA.name < tagB.name ? -1 : 1)
|
||||||
setTags (tagsTmp)
|
setTags (tagsTmp)
|
||||||
} catch (error) {
|
setTagsCounts (tagsCountsTmp)
|
||||||
console.error('Failed to fetch tags:', error)
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error ('Failed to fetch tags:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchTags()
|
fetchTags ()
|
||||||
}, [postId])
|
}, [posts])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-64 bg-gray-100 p-4 border-r border-gray-200 h-full">
|
<div className="w-64 bg-gray-100 p-4 border-r border-gray-200 h-full">
|
||||||
<TagSearch />
|
<TagSearch />
|
||||||
{['general', 'deerjikist', 'nico'].map (cat => (cat in tags) ? (
|
{['general', 'deerjikist', 'nico'].map (cat => cat in tags && <>
|
||||||
<>
|
<h2>{tagNameMap[cat]}</h2>
|
||||||
<h2>{tagNameMap[cat]}</h2>
|
<ul>
|
||||||
<ul>
|
{tags[cat].map (tag => (
|
||||||
{tags[cat].map (tag => (
|
<li key={tag.id} className="mb-2">
|
||||||
<li key={tag.id} className="mb-2">
|
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
|
||||||
<Link
|
className="text-blue-600 hover:underline">
|
||||||
to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
|
{tag.name}
|
||||||
className="text-blue-600 hover:underline"
|
</Link>
|
||||||
>
|
{posts.length > 1 && <span className="ml-1">{tagsCounts[tag.id]}</span>}
|
||||||
{tag.name}
|
</li>))}
|
||||||
</Link>
|
</ul>
|
||||||
</li>
|
</>)}
|
||||||
))}
|
</div>)
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
) : <></>)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default TagSidebar
|
export default TagSidebar
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import React from "react"
|
import React from "react"
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
const TopNav: React.FC = () => {
|
const TopNav: React.FC = () => (
|
||||||
return (
|
|
||||||
<nav className="bg-gray-800 text-white p-3 flex justify-between items-center w-full">
|
<nav className="bg-gray-800 text-white p-3 flex justify-between items-center w-full">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<a href="/" className="text-xl font-bold text-orange-500">ぼざクリ タグ広場</a>
|
<Link to="/" className="text-xl font-bold text-orange-500">ぼざクリ タグ広場</Link>
|
||||||
<a href="/" className="hover:text-orange-500">広場</a>
|
<Link to="/posts" className="hover:text-orange-500">広場</Link>
|
||||||
<a href="/deerjikists" className="hover:text-orange-500">ニジラー</a>
|
<Link to="/deerjikists" className="hover:text-orange-500">ニジラー</Link>
|
||||||
<a href="/tags" className="hover:text-orange-500">タグ</a>
|
<Link to="/tags" className="hover:text-orange-500">タグ</Link>
|
||||||
<a href="/wiki" className="hover:text-orange-500">Wiki</a>
|
<Link to="/wiki" className="hover:text-orange-500">Wiki</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto pr-4">
|
<div className="ml-auto pr-4">
|
||||||
<a href="/login" className="hover:text-orange-500">ログイン</a>
|
<Link to="/login" className="hover:text-orange-500">ログイン</Link>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TopNav
|
export default TopNav
|
||||||
|
|||||||
@@ -4,15 +4,23 @@ import axios from 'axios'
|
|||||||
import { API_BASE_URL, SITE_TITLE } from '../config'
|
import { API_BASE_URL, SITE_TITLE } from '../config'
|
||||||
import NicoViewer from '../components/NicoViewer'
|
import NicoViewer from '../components/NicoViewer'
|
||||||
|
|
||||||
|
type Tag = { id: number
|
||||||
|
name: string
|
||||||
|
category: string }
|
||||||
|
|
||||||
type Post = { id: number
|
type Post = { id: number
|
||||||
url: string
|
url: string
|
||||||
title: string
|
title: string
|
||||||
thumbnail: string
|
thumbnail: string
|
||||||
tags: { name: string }[] }
|
tags: Tag[] }
|
||||||
|
|
||||||
const PostDetailPage = () => {
|
type Props = { posts: Post[]
|
||||||
|
setPosts: (posts: Post[]) => void }
|
||||||
|
|
||||||
|
|
||||||
|
const PostDetailPage = (props: Props) => {
|
||||||
|
const { posts, setPosts } = props
|
||||||
const { id } = useParams ()
|
const { id } = useParams ()
|
||||||
const [post, setPost] = useState<Post | null> (null)
|
|
||||||
|
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
|
|
||||||
@@ -20,28 +28,36 @@ const PostDetailPage = () => {
|
|||||||
if (!(id))
|
if (!(id))
|
||||||
return
|
return
|
||||||
void (axios.get (`/api/posts/${ id }`)
|
void (axios.get (`/api/posts/${ id }`)
|
||||||
.then (res => setPost (res.data))
|
.then (res => setPosts ([res.data]))
|
||||||
.catch (err => console.error ('うんち!', err)))
|
.catch (err => console.error ('うんち!', err)))
|
||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
if (!(post))
|
if (!(posts.length))
|
||||||
return <div>Loading...</div>
|
return <div>Loading...</div>
|
||||||
|
|
||||||
document.title = `${ post.url } | ${ SITE_TITLE }`
|
const post = posts[0]
|
||||||
|
|
||||||
|
document.title = `${ post.title || post.url } | ${ SITE_TITLE }`
|
||||||
|
|
||||||
const url = new URL (post.url)
|
const url = new URL (post.url)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{url.hostname.split ('.').slice (-2).join ('.') === 'nicovideo.jp' ? (
|
{(() => {
|
||||||
<NicoViewer id={url.pathname.match (/(?<=\/watch\/)[a-zA-Z0-9]+?(?=\/|$)/)[0]}
|
if (url.hostname.split ('.').slice (-2).join ('.') === 'nicovideo.jp')
|
||||||
width="640"
|
{
|
||||||
height="360" />
|
return (
|
||||||
) : (
|
<NicoViewer
|
||||||
<img src={post.thumbnail} alt={post.url} className="mb-4 w-full" />
|
id={url.pathname.match (
|
||||||
)}
|
/(?<=\/watch\/)[a-zA-Z0-9]+?(?=\/|$)/)[0]}
|
||||||
</div>
|
width="640"
|
||||||
)
|
height="360" />)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return <img src={post.thumbnail} alt={post.url} className="mb-4 w-full" />
|
||||||
|
}) ()}
|
||||||
|
</div>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default PostDetailPage
|
export default PostDetailPage
|
||||||
|
|||||||
@@ -3,8 +3,22 @@ import { Link, useLocation } from 'react-router-dom'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { API_BASE_URL, SITE_TITLE } from '../config'
|
import { API_BASE_URL, SITE_TITLE } from '../config'
|
||||||
|
|
||||||
const PostPage = () => {
|
type Tag = { id: number
|
||||||
const [posts, setPosts] = useState([])
|
name: string
|
||||||
|
category: string }
|
||||||
|
|
||||||
|
type Post = { id: number
|
||||||
|
url: string
|
||||||
|
title: string
|
||||||
|
thumbnail: string
|
||||||
|
tags: Tag[] }
|
||||||
|
|
||||||
|
type Props = { posts: Post[]
|
||||||
|
setPosts: (posts: Post[]) => void }
|
||||||
|
|
||||||
|
|
||||||
|
const PostPage = (props: Props) => {
|
||||||
|
const { posts, setPosts } = props
|
||||||
|
|
||||||
const location = useLocation ()
|
const location = useLocation ()
|
||||||
const query = new URLSearchParams (location.search)
|
const query = new URLSearchParams (location.search)
|
||||||
@@ -19,35 +33,34 @@ const PostPage = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchPosts = async () => {
|
const fetchPosts = async () => {
|
||||||
try {
|
try
|
||||||
const response = await axios.get(`${ API_BASE_URL }/posts`, {
|
{
|
||||||
|
const res = await axios.get (`${ API_BASE_URL }/posts`, {
|
||||||
params: { tags: tags.join (','),
|
params: { tags: tags.join (','),
|
||||||
match: (anyFlg ? 'any' : 'all') } })
|
match: (anyFlg ? 'any' : 'all') } })
|
||||||
setPosts(response.data)
|
setPosts (res.data)
|
||||||
} catch (error) {
|
}
|
||||||
console.error('Failed to fetch posts:', error)
|
catch (error)
|
||||||
setPosts([])
|
{
|
||||||
|
console.error ('Failed to fetch posts:', error)
|
||||||
|
setPosts ([])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fetchPosts()
|
fetchPosts()
|
||||||
}, [location.search])
|
}, [location.search])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-4 p-4">
|
<div className="flex flex-wrap gap-4 p-4">
|
||||||
{posts.map (post => (
|
{posts.map (post => (
|
||||||
<Link
|
<Link to={`/posts/${ post.id }`}
|
||||||
to={`/posts/${ post.id }`}
|
key={post.id}
|
||||||
key={post.id}
|
className="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg">
|
||||||
className="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg"
|
<img src={post.thumbnail ?? post.thumbnail_base}
|
||||||
>
|
className="object-none w-full h-full" />
|
||||||
<img
|
</Link>
|
||||||
src={post.thumbnail}
|
))}
|
||||||
className="object-none w-full h-full"
|
</div>)
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default PostPage
|
export default PostPage
|
||||||
|
|||||||
Reference in New Issue
Block a user