みてるぞ 2 weeks ago
parent
commit
f47d7bbb87
4 changed files with 82 additions and 32 deletions
  1. +25
    -12
      backend/app/controllers/posts_controller.rb
  2. +10
    -2
      frontend/src/components/TagSidebar.tsx
  3. +10
    -7
      frontend/src/pages/posts/PostDetailPage.tsx
  4. +37
    -11
      frontend/src/pages/posts/PostListPage.tsx

+ 25
- 12
backend/app/controllers/posts_controller.rb View File

@@ -5,16 +5,26 @@ require 'nokogiri'
class PostsController < ApplicationController class PostsController < ApplicationController
# GET /posts # GET /posts
def index def index
latest_id = params[:latest].to_i
offset = params[:offset].to_i

posts = filtered_posts.order(created_at: :desc)
posts = if posts.first&.id == latest_id
posts.limit(20).offset(offset)
else
posts.limit(offset + 20)
end
render json: posts.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } })
limit = (params[:limit] || 20).to_i
cursor = params[:cursor]

q = filtered_posts.order(created_at: :desc)

next_cursor = nil
if cursor.present?
q = q.where('posts.created_at < ?', Time.iso8601(cursor))
end

posts = q.limit(limit + 1)

next_cursor = nil
if posts.size > limit
next_cursor = posts.last.created_at.iso8601(6)
posts = posts.first(limit)
end

render json: { posts: posts.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }),
next_cursor: }
end end


def random def random
@@ -31,7 +41,10 @@ class PostsController < ApplicationController
# GET /posts/1 # GET /posts/1
def show def show
post = Post.includes(:tags).find(params[:id]) post = Post.includes(:tags).find(params[:id])
viewed = current_user&.viewed?(post)
return head :not_found unless post

viewed = current_user&.viewed?(post) || false

render json: (post render json: (post
.as_json(include: { tags: { only: [:id, :name, :category] } }) .as_json(include: { tags: { only: [:id, :name, :category] } })
.merge(viewed: viewed)) .merge(viewed: viewed))
@@ -95,7 +108,7 @@ class PostsController < ApplicationController
private private


def filtered_posts def filtered_posts
tag_names = params[:tags]&.split(',')
tag_names = params[:tags]&.split(?\ )
match_type = params[:match] match_type = params[:match]
tag_names.present? ? filter_posts_by_tags(tag_names, match_type) : Post.all tag_names.present? ? filter_posts_by_tags(tag_names, match_type) : Post.all
end end


+ 10
- 2
frontend/src/components/TagSidebar.tsx View File

@@ -7,6 +7,7 @@ import TagSearch from '@/components/TagSearch'
import SectionTitle from '@/components/common/SectionTitle' import SectionTitle from '@/components/common/SectionTitle'
import SidebarComponent from '@/components/layout/SidebarComponent' import SidebarComponent from '@/components/layout/SidebarComponent'
import { API_BASE_URL } from '@/config' import { API_BASE_URL } from '@/config'
import { CATEGORIES } from '@/consts'


import type { Post, Tag } from '@/types' import type { Post, Tag } from '@/types'


@@ -27,6 +28,8 @@ export default ({ posts }: Props) => {


useEffect (() => { useEffect (() => {
const tagsTmp: TagByCategory = { } const tagsTmp: TagByCategory = { }
let cnt = 0
loop:
for (const post of posts) for (const post of posts)
{ {
for (const tag of post.tags) for (const tag of post.tags)
@@ -34,7 +37,12 @@ export default ({ posts }: Props) => {
if (!(tag.category in tagsTmp)) if (!(tag.category in tagsTmp))
tagsTmp[tag.category] = [] tagsTmp[tag.category] = []
if (!(tagsTmp[tag.category].map (t => t.id).includes (tag.id))) if (!(tagsTmp[tag.category].map (t => t.id).includes (tag.id)))
tagsTmp[tag.category].push (tag)
{
tagsTmp[tag.category].push (tag)
++cnt
if (cnt >= 25)
break loop
}
} }
} }
for (const cat of Object.keys (tagsTmp)) for (const cat of Object.keys (tagsTmp))
@@ -47,7 +55,7 @@ export default ({ posts }: Props) => {
<TagSearch /> <TagSearch />
<SectionTitle>タグ</SectionTitle> <SectionTitle>タグ</SectionTitle>
<ul> <ul>
{['general', 'deerjikist', 'nico'].map (cat => cat in tags && (
{CATEGORIES.map (cat => cat in tags && (
<> <>
{tags[cat].map (tag => ( {tags[cat].map (tag => (
<li key={tag.id} className="mb-1"> <li key={tag.id} className="mb-1">


+ 10
- 7
frontend/src/pages/posts/PostDetailPage.tsx View File

@@ -1,4 +1,5 @@
import axios from 'axios' import axios from 'axios'
import toCamel from 'camelcase-keys'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { Link, useLocation, useParams } from 'react-router-dom' import { Link, useLocation, useParams } from 'react-router-dom'
@@ -32,9 +33,9 @@ export default ({ user }: Props) => {
void (axios.delete ( void (axios.delete (
`${ API_BASE_URL }/posts/${ id }/viewed`, `${ API_BASE_URL }/posts/${ id }/viewed`,
{ headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } }) { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
.then (res => setPost (post => ({ ...post, viewed: false })))
.catch (err => toast ({ title: '失敗……',
description: '通信に失敗しました……' })))
.then (() => setPost (post => ({ ...post, viewed: false })))
.catch (() => toast ({ title: '失敗……',
description: '通信に失敗しました……' })))
} }
else else
{ {
@@ -42,9 +43,9 @@ export default ({ user }: Props) => {
`${ API_BASE_URL }/posts/${ id }/viewed`, `${ API_BASE_URL }/posts/${ id }/viewed`,
{ }, { },
{ headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } }) { headers: { 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
.then (res => setPost (post => ({ ...post, viewed: true })))
.catch (err => toast ({ title: '失敗……',
description: '通信に失敗しました……' })))
.then (() => setPost (post => ({ ...post, viewed: true })))
.catch (() => toast ({ title: '失敗……',
description: '通信に失敗しました……' })))
} }
} }


@@ -53,7 +54,7 @@ export default ({ user }: Props) => {
return return
void (axios.get (`${ API_BASE_URL }/posts/${ id }`, { headers: { void (axios.get (`${ API_BASE_URL }/posts/${ id }`, { headers: {
'X-Transfer-Code': localStorage.getItem ('user_code') || '' } }) 'X-Transfer-Code': localStorage.getItem ('user_code') || '' } })
.then (res => setPost (res.data))
.then (res => setPost (toCamel (res.data, { deep: true })))
.catch (err => console.error ('うんち!', err))) .catch (err => console.error ('うんち!', err)))
}, [id]) }, [id])


@@ -78,6 +79,8 @@ export default ({ user }: Props) => {
return ( return (
<> <>
<Helmet> <Helmet>
{(post?.thumbnail || post?.thumbnailBase) &&
<meta name="thumbnail" content={post.thumbnail || post.thumbnailBase} />}
{post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>} {post && <title>{`${ post.title || post.url } | ${ SITE_TITLE }`}</title>}
</Helmet> </Helmet>
<TagDetailSidebar post={post} /> <TagDetailSidebar post={post} />


+ 37
- 11
frontend/src/pages/posts/PostListPage.tsx View File

@@ -1,6 +1,6 @@
import axios from 'axios' import axios from 'axios'
import toCamel from 'camelcase-keys' import toCamel from 'camelcase-keys'
import React, { useEffect, useState } from 'react'
import React, { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { Link, useLocation } from 'react-router-dom' import { Link, useLocation } from 'react-router-dom'


@@ -16,6 +16,22 @@ import type { Post, Tag, WikiPage } from '@/types'
export default () => { export default () => {
const [posts, setPosts] = useState<Post[] | null> (null) const [posts, setPosts] = useState<Post[] | null> (null)
const [wikiPage, setWikiPage] = useState<WikiPage | null> (null) const [wikiPage, setWikiPage] = useState<WikiPage | null> (null)
const [cursor, setCursor] = useState ('')
const [loading, setLoading] = useState (false)

const loaderRef = useRef<HTMLDivElement | null> (null)

const loadMore = async () => {
setLoading (true)
const res = await axios.get (`${ API_BASE_URL }/posts`, {
params: { tags: tags.join (' '),
match: (anyFlg ? 'any' : 'all'),
cursor } })
const data = toCamel (res.data, { deep: true })
setPosts (posts => [...(posts || []), ...data.posts])
setCursor (data.nextCursor)
setLoading (false)
}


const location = useLocation () const location = useLocation ()
const query = new URLSearchParams (location.search) const query = new URLSearchParams (location.search)
@@ -24,14 +40,22 @@ export default () => {
const tags = tagsQuery.split (' ').filter (e => e !== '') const tags = tagsQuery.split (' ').filter (e => e !== '')


useEffect(() => { useEffect(() => {
void (axios.get (`${ API_BASE_URL }/posts`, {
params: { tags: tags.join (','),
match: (anyFlg ? 'any' : 'all') } })
.then (res => setPosts (toCamel (res.data, { deep: true })))
.catch (err => {
console.error ('Failed to fetch posts:', err)
setPosts ([])
}))
const observer = new IntersectionObserver (entries => {
if (entries[0].isIntersecting && !(loading) && cursor)
loadMore ()
}, { threshold: 1 })

const target = loaderRef.current
target && observer.observe (target)

return () => target && observer.unobserve (target)
}, [loaderRef, loading])

useEffect (() => {
console.log ('test')
setCursor ('')
setPosts (null)
loadMore ()


setWikiPage (null) setWikiPage (null)
if (tags.length === 1) if (tags.length === 1)
@@ -50,10 +74,10 @@ export default () => {
<title> <title>
{tags.length {tags.length
? `${ tags.join (anyFlg ? ' or ' : ' and ') } | ${ SITE_TITLE }` ? `${ tags.join (anyFlg ? ' or ' : ' and ') } | ${ SITE_TITLE }`
: `${ SITE_TITLE } 〜 ぼざクリも、ぼざろ外も、外伝もあるんだよ`}
: `${ SITE_TITLE } 〜 ぼざクリ関聯綜合リンク集サイト`}
</title> </title>
</Helmet> </Helmet>
<TagSidebar posts={posts || []} />
<TagSidebar posts={posts?.slice (0, 20) || []} />
<MainArea> <MainArea>
<TabGroup key={wikiPage}> <TabGroup key={wikiPage}>
<Tab name="広場"> <Tab name="広場">
@@ -66,10 +90,12 @@ export default () => {
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.thumbnailBase} <img src={post.thumbnail ?? post.thumbnailBase}
alt={post.title || post.url}
className="object-none w-full h-full" /> className="object-none w-full h-full" />
</Link>))} </Link>))}
</div>) </div>)
: '広場には何もありませんよ.')} : '広場には何もありませんよ.')}
<div ref={loaderRef} className="h-12"></div>
</Tab> </Tab>
{(wikiPage && wikiPage.body) && ( {(wikiPage && wikiPage.body) && (
<Tab name="Wiki" init={posts && !(posts.length)}> <Tab name="Wiki" init={posts && !(posts.length)}>


Loading…
Cancel
Save