コミットを比較
5 コミット
214c91e3bf
...
7df51fb34b
| 作成者 | SHA1 | 日付 | |
|---|---|---|---|
| 7df51fb34b | |||
| 4e00ec40ab | |||
| 06cd569fc5 | |||
| 8a126e2e92 | |||
| 5cc47e42e1 |
@@ -110,7 +110,8 @@ class PostsController < ApplicationController
|
|||||||
original_created_before = params[:original_created_before]
|
original_created_before = params[:original_created_before]
|
||||||
|
|
||||||
post = Post.find(params[:id].to_i)
|
post = Post.find(params[:id].to_i)
|
||||||
tags = post.tags.where(category: 'nico').to_a + Tag.normalise_tags(tag_names)
|
tags = post.tags.where(category: 'nico').to_a +
|
||||||
|
Tag.normalise_tags(tag_names, with_tagme: false)
|
||||||
if post.update(title:, tags:, original_created_from:, original_created_before:)
|
if post.update(title:, tags:, original_created_from:, original_created_before:)
|
||||||
render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }),
|
render json: post.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }),
|
||||||
status: :ok
|
status: :ok
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ class UsersController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def verify
|
def verify
|
||||||
|
ip_bin = IPAddr.new(request.remote_ip).hton
|
||||||
|
ip_address = IpAddress.find_or_create_by!(ip_address: ip_bin)
|
||||||
|
|
||||||
user = User.find_by(inheritance_code: params[:code])
|
user = User.find_by(inheritance_code: params[:code])
|
||||||
render json: if user
|
return render json: { valid: false } unless user
|
||||||
{ valid: true, user: user.slice(:id, :name, :inheritance_code, :role) }
|
|
||||||
else
|
UserIp.find_or_create_by!(user:, ip_address:)
|
||||||
{ valid: false }
|
|
||||||
end
|
render json: { valid: true, user: user.slice(:id, :name, :inheritance_code, :role) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def renew
|
def renew
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class Tag < ApplicationRecord
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
tags << Tag.tagme if with_tagme && tags.size < 20 && tags.none?(Tag.tagme)
|
tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme)
|
||||||
tags.uniq
|
tags.uniq
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
has_many :posts
|
has_many :posts
|
||||||
has_many :settings
|
has_many :settings
|
||||||
has_many :ip_addresses
|
|
||||||
has_many :user_ips, dependent: :destroy
|
has_many :user_ips, dependent: :destroy
|
||||||
has_many :ip_addresses, through: :user_ips
|
has_many :ip_addresses, through: :user_ips
|
||||||
has_many :user_post_views, dependent: :destroy
|
has_many :user_post_views, dependent: :destroy
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class RenameIpAdressColumnToIpAddresses < ActiveRecord::Migration[8.0]
|
||||||
|
def change
|
||||||
|
rename_column :ip_addresses, :ip_adress, :ip_address
|
||||||
|
end
|
||||||
|
end
|
||||||
生成ファイル
+20
-2
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do
|
ActiveRecord::Schema[8.0].define(version: 2025_11_26_231500) do
|
||||||
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.string "name", null: false
|
t.string "name", null: false
|
||||||
t.string "record_type", null: false
|
t.string "record_type", null: false
|
||||||
@@ -40,7 +40,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
create_table "ip_addresses", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "ip_addresses", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.binary "ip_adress", limit: 16, null: false
|
t.binary "ip_address", limit: 16, null: false
|
||||||
t.boolean "banned", default: false, null: false
|
t.boolean "banned", default: false, null: false
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
@@ -70,9 +70,16 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do
|
|||||||
t.bigint "deleted_user_id"
|
t.bigint "deleted_user_id"
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
|
t.datetime "discarded_at"
|
||||||
|
t.virtual "is_active", type: :boolean, as: "(`discarded_at` is null)", stored: true
|
||||||
|
t.virtual "active_unique_key", type: :string, as: "(case when (`discarded_at` is null) then concat(`post_id`,_utf8mb4':',`tag_id`) else NULL end)", stored: true
|
||||||
|
t.index ["active_unique_key"], name: "idx_post_tags_active_unique", unique: true
|
||||||
t.index ["created_user_id"], name: "index_post_tags_on_created_user_id"
|
t.index ["created_user_id"], name: "index_post_tags_on_created_user_id"
|
||||||
t.index ["deleted_user_id"], name: "index_post_tags_on_deleted_user_id"
|
t.index ["deleted_user_id"], name: "index_post_tags_on_deleted_user_id"
|
||||||
|
t.index ["discarded_at"], name: "index_post_tags_on_discarded_at"
|
||||||
|
t.index ["post_id", "discarded_at"], name: "index_post_tags_on_post_id_and_discarded_at"
|
||||||
t.index ["post_id"], name: "index_post_tags_on_post_id"
|
t.index ["post_id"], name: "index_post_tags_on_post_id"
|
||||||
|
t.index ["tag_id", "discarded_at"], name: "index_post_tags_on_tag_id_and_discarded_at"
|
||||||
t.index ["tag_id"], name: "index_post_tags_on_tag_id"
|
t.index ["tag_id"], name: "index_post_tags_on_tag_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -107,6 +114,15 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do
|
|||||||
t.index ["tag_id"], name: "index_tag_aliases_on_tag_id"
|
t.index ["tag_id"], name: "index_tag_aliases_on_tag_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
create_table "tag_implications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
|
t.bigint "tag_id", null: false
|
||||||
|
t.bigint "parent_tag_id", null: false
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
t.index ["parent_tag_id"], name: "index_tag_implications_on_parent_tag_id"
|
||||||
|
t.index ["tag_id"], name: "index_tag_implications_on_tag_id"
|
||||||
|
end
|
||||||
|
|
||||||
create_table "tag_similarities", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
create_table "tag_similarities", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
|
||||||
t.bigint "tag_id", null: false
|
t.bigint "tag_id", null: false
|
||||||
t.bigint "target_tag_id", null: false
|
t.bigint "target_tag_id", null: false
|
||||||
@@ -176,6 +192,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_09_075500) do
|
|||||||
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
add_foreign_key "posts", "users", column: "uploaded_user_id"
|
||||||
add_foreign_key "settings", "users"
|
add_foreign_key "settings", "users"
|
||||||
add_foreign_key "tag_aliases", "tags"
|
add_foreign_key "tag_aliases", "tags"
|
||||||
|
add_foreign_key "tag_implications", "tags"
|
||||||
|
add_foreign_key "tag_implications", "tags", column: "parent_tag_id"
|
||||||
add_foreign_key "tag_similarities", "tags"
|
add_foreign_key "tag_similarities", "tags"
|
||||||
add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
|
add_foreign_key "tag_similarities", "tags", column: "target_tag_id"
|
||||||
add_foreign_key "user_ips", "ip_addresses"
|
add_foreign_key "user_ips", "ip_addresses"
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
namespace :nico do
|
|
||||||
desc 'ニコタグ連携'
|
|
||||||
task link: :environment do
|
|
||||||
Post.find_each do |post|
|
|
||||||
tags = post.tags.where(category: 'nico')
|
|
||||||
tags.each do |tag|
|
|
||||||
post.tags.concat(tag.linked_tags) if tag.linked_tags.present?
|
|
||||||
end
|
|
||||||
post.tags = post.tags.to_a.uniq
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -51,7 +51,7 @@ namespace :nico do
|
|||||||
end
|
end
|
||||||
tags_to_add.concat([tag] + tag.linked_tags)
|
tags_to_add.concat([tag] + tag.linked_tags)
|
||||||
end
|
end
|
||||||
tags_to_add << Tag.tagme if post.tags.size < 20
|
tags_to_add << Tag.tagme if post.tags.size < 10
|
||||||
tags_to_add << Tag.bot
|
tags_to_add << Tag.bot
|
||||||
post.tags = (post.tags + tags_to_add).uniq
|
post.tags = (post.tags + tags_to_add).uniq
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query'
|
import { useQueryClient } from '@tanstack/react-query'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { createPath, useNavigate } from 'react-router-dom'
|
||||||
|
|
||||||
import { useOverlayStore } from '@/components/RouteBlockerOverlay'
|
import { useOverlayStore } from '@/components/RouteBlockerOverlay'
|
||||||
import { prefetchForURL } from '@/lib/prefetchers'
|
import { prefetchForURL } from '@/lib/prefetchers'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import type { AnchorHTMLAttributes, FC, MouseEvent, TouchEvent } from 'react'
|
import type { AnchorHTMLAttributes, FC, MouseEvent, TouchEvent } from 'react'
|
||||||
|
import type { To } from 'react-router-dom'
|
||||||
|
|
||||||
type Props = AnchorHTMLAttributes<HTMLAnchorElement> & {
|
type Props = AnchorHTMLAttributes<HTMLAnchorElement> & {
|
||||||
to: string
|
to: To
|
||||||
replace?: boolean
|
replace?: boolean
|
||||||
className?: string
|
className?: string
|
||||||
cancelOnError?: boolean }
|
cancelOnError?: boolean }
|
||||||
@@ -25,7 +26,10 @@ export default (({ to,
|
|||||||
...rest }: Props) => {
|
...rest }: Props) => {
|
||||||
const navigate = useNavigate ()
|
const navigate = useNavigate ()
|
||||||
const qc = useQueryClient ()
|
const qc = useQueryClient ()
|
||||||
const url = useMemo (() => (new URL (to, location.origin)).toString (), [to])
|
const url = useMemo (() => {
|
||||||
|
const path = (typeof to === 'string') ? to : createPath (to)
|
||||||
|
return (new URL (path, location.origin)).toString ()
|
||||||
|
}, [to])
|
||||||
const setOverlay = useOverlayStore (s => s.setActive)
|
const setOverlay = useOverlayStore (s => s.setActive)
|
||||||
|
|
||||||
const doPrefetch = async () => {
|
const doPrefetch = async () => {
|
||||||
@@ -74,7 +78,7 @@ export default (({ to,
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={to}
|
<a href={typeof to === 'string' ? to : createPath (to)}
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
onTouchStart={handleTouchStart}
|
onTouchStart={handleTouchStart}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
import PrefetchLink from '@/components/PrefetchLink'
|
||||||
import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts'
|
import { LIGHT_COLOUR_SHADE, DARK_COLOUR_SHADE, TAG_COLOUR } from '@/consts'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
@@ -9,7 +10,8 @@ import type { Tag } from '@/types'
|
|||||||
|
|
||||||
type CommonProps = { tag: Tag
|
type CommonProps = { tag: Tag
|
||||||
withWiki?: boolean
|
withWiki?: boolean
|
||||||
withCount?: boolean }
|
withCount?: boolean
|
||||||
|
prefetch?: boolean }
|
||||||
|
|
||||||
type PropsWithLink =
|
type PropsWithLink =
|
||||||
CommonProps & { linkFlg?: true } & Partial<ComponentProps<typeof Link>>
|
CommonProps & { linkFlg?: true } & Partial<ComponentProps<typeof Link>>
|
||||||
@@ -24,6 +26,7 @@ export default (({ tag,
|
|||||||
linkFlg = true,
|
linkFlg = true,
|
||||||
withWiki = true,
|
withWiki = true,
|
||||||
withCount = true,
|
withCount = true,
|
||||||
|
prefetch = false,
|
||||||
...props }: Props) => {
|
...props }: Props) => {
|
||||||
const spanClass = cn (
|
const spanClass = cn (
|
||||||
`text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE }`,
|
`text-${ TAG_COLOUR[tag.category] }-${ LIGHT_COLOUR_SHADE }`,
|
||||||
@@ -44,11 +47,19 @@ export default (({ tag,
|
|||||||
</span>)}
|
</span>)}
|
||||||
{linkFlg
|
{linkFlg
|
||||||
? (
|
? (
|
||||||
<Link to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
|
prefetch
|
||||||
|
? <PrefetchLink
|
||||||
|
to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
|
||||||
className={linkClass}
|
className={linkClass}
|
||||||
{...props}>
|
{...props}>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</Link>)
|
</PrefetchLink>
|
||||||
|
: <Link
|
||||||
|
to={`/posts?${ (new URLSearchParams ({ tags: tag.name })).toString () }`}
|
||||||
|
className={linkClass}
|
||||||
|
{...props}>
|
||||||
|
{tag.name}
|
||||||
|
</Link>)
|
||||||
: (
|
: (
|
||||||
<span className={spanClass}
|
<span className={spanClass}
|
||||||
{...props}>
|
{...props}>
|
||||||
|
|||||||
@@ -10,16 +10,17 @@ import { API_BASE_URL } from '@/config'
|
|||||||
import { CATEGORIES } from '@/consts'
|
import { CATEGORIES } from '@/consts'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC, MouseEvent } from 'react'
|
||||||
|
|
||||||
import type { Post, Tag } from '@/types'
|
import type { Post, Tag } from '@/types'
|
||||||
|
|
||||||
type TagByCategory = Record<string, Tag[]>
|
type TagByCategory = Record<string, Tag[]>
|
||||||
|
|
||||||
type Props = { posts: Post[] }
|
type Props = { posts: Post[]
|
||||||
|
onClick?: (event: MouseEvent<HTMLElement>) => void }
|
||||||
|
|
||||||
|
|
||||||
export default (({ posts }: Props) => {
|
export default (({ posts, onClick }: Props) => {
|
||||||
const navigate = useNavigate ()
|
const navigate = useNavigate ()
|
||||||
|
|
||||||
const [tagsVsbl, setTagsVsbl] = useState (false)
|
const [tagsVsbl, setTagsVsbl] = useState (false)
|
||||||
@@ -64,11 +65,11 @@ export default (({ posts }: Props) => {
|
|||||||
<div className={cn (!(tagsVsbl) && 'hidden', 'md:block mt-4')}>
|
<div className={cn (!(tagsVsbl) && 'hidden', 'md:block mt-4')}>
|
||||||
<SectionTitle>タグ</SectionTitle>
|
<SectionTitle>タグ</SectionTitle>
|
||||||
<ul>
|
<ul>
|
||||||
{CATEGORIES.flatMap (cat => cat in tags ? (
|
{CATEGORIES.flatMap (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">
|
||||||
<TagLink tag={tag}/>
|
<TagLink tag={tag} prefetch onClick={onClick}/>
|
||||||
</li>))) : [])}
|
</li>)) : [])}
|
||||||
</ul>
|
</ul>
|
||||||
<SectionTitle>関聯</SectionTitle>
|
<SectionTitle>関聯</SectionTitle>
|
||||||
{posts.length > 0 && (
|
{posts.length > 0 && (
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import WikiBody from '@/components/WikiBody'
|
|||||||
import TabGroup, { Tab } from '@/components/common/TabGroup'
|
import TabGroup, { Tab } from '@/components/common/TabGroup'
|
||||||
import MainArea from '@/components/layout/MainArea'
|
import MainArea from '@/components/layout/MainArea'
|
||||||
import { API_BASE_URL, SITE_TITLE } from '@/config'
|
import { API_BASE_URL, SITE_TITLE } from '@/config'
|
||||||
|
import { fetchPosts } from '@/lib/posts'
|
||||||
|
|
||||||
import type { Post, WikiPage } from '@/types'
|
import type { Post, WikiPage } from '@/types'
|
||||||
|
|
||||||
@@ -28,13 +29,11 @@ export default () => {
|
|||||||
const loadMore = async (withCursor: boolean) => {
|
const loadMore = async (withCursor: boolean) => {
|
||||||
setLoading (true)
|
setLoading (true)
|
||||||
|
|
||||||
const res = await axios.get (`${ API_BASE_URL }/posts`, {
|
const data = await fetchPosts ({
|
||||||
params: { tags: tags.join (' '),
|
tags: tags.join (' '),
|
||||||
match: anyFlg ? 'any' : 'all',
|
match: anyFlg ? 'any' : 'all',
|
||||||
limit: '20',
|
limit: 20,
|
||||||
...(withCursor && { cursor }) } })
|
...(withCursor && { cursor }) })
|
||||||
const data = toCamel (res.data as any, { deep: true }) as { posts: Post[]
|
|
||||||
nextCursor: string }
|
|
||||||
setPosts (posts => (
|
setPosts (posts => (
|
||||||
[...((new Map ([...(withCursor ? posts : []), ...data.posts]
|
[...((new Map ([...(withCursor ? posts : []), ...data.posts]
|
||||||
.map (post => [post.id, post])))
|
.map (post => [post.id, post])))
|
||||||
@@ -111,7 +110,13 @@ export default () => {
|
|||||||
</title>
|
</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<TagSidebar posts={posts.slice (0, 20)}/>
|
<TagSidebar posts={posts.slice (0, 20)} onClick={() => {
|
||||||
|
const statesToSave = {
|
||||||
|
posts, cursor,
|
||||||
|
scroll: containerRef.current?.scrollTop ?? 0 }
|
||||||
|
sessionStorage.setItem (`posts:${ tagsQuery }`,
|
||||||
|
JSON.stringify (statesToSave))
|
||||||
|
}}/>
|
||||||
|
|
||||||
<MainArea>
|
<MainArea>
|
||||||
<TabGroup>
|
<TabGroup>
|
||||||
|
|||||||
新しい課題から参照
ユーザをブロックする