diff --git a/backend/app/controllers/posts_controller.rb b/backend/app/controllers/posts_controller.rb index 9116f9b..3385616 100644 --- a/backend/app/controllers/posts_controller.rb +++ b/backend/app/controllers/posts_controller.rb @@ -6,12 +6,12 @@ class PostsController < ApplicationController # GET /posts def index posts = filtered_posts - render json: posts.as_json(include: { tags: { only: [:id, :name, :category] } }) + render json: posts.as_json(include: { tags: { only: [:id, :name, :category, :post_count] } }) end def random post = filtered_posts.order('RAND()').first - viewed = current_user&.viewed?(post) + viewed = current_user&.viewed?(post) || false render json: (post .as_json(include: { tags: { only: [:id, :name, :category] } }) .merge(viewed: viewed)) diff --git a/backend/app/controllers/tags_controller.rb b/backend/app/controllers/tags_controller.rb index fb967c5..6597a43 100644 --- a/backend/app/controllers/tags_controller.rb +++ b/backend/app/controllers/tags_controller.rb @@ -1,11 +1,11 @@ class TagsController < ApplicationController def index - tags = - if params[:post].present? - Tag.joins(:posts).where(posts: { id: params[:post] }) - else - Tag.all - end + post_id = params[:post] + tags = if post_id.present? + Tag.joins(:posts).where(posts: { id: post_id }) + else + Tag.all + end render json: tags end @@ -14,23 +14,25 @@ class TagsController < ApplicationController return render json: [] if q.blank? tags = (Tag - .left_joins(:posts) - .select('tags.id, tags.name, tags.category, COUNT(posts.id) AS post_count') - .where('(tags.category = ? AND tags.name LIKE ?) - OR tags.name LIKE ?', + .where('(category = ? AND name LIKE ?) OR name LIKE ?', 'nico', "nico:#{ q }%", "#{ q }%") - .group('tags.id') - .order('post_count DESC, tags.name ASC') + .order('post_count DESC, name ASC') .limit(20)) - render json: tags.map { |tag| { - id: tag.id, - name: tag.name, - category: tag.category, - count: tag.post_count } } + render json: tags end def show - render json: Tag.find(params[:id]) + tag = Tag.find(params[:id]) + render json: tag + end + + def show_by_name + tag = Tag.find_by(name: params[:name]) + if tag + render json: tag + else + head :not_found + end end def create diff --git a/backend/app/controllers/wiki_pages_controller.rb b/backend/app/controllers/wiki_pages_controller.rb index c792fb7..1ac21a8 100644 --- a/backend/app/controllers/wiki_pages_controller.rb +++ b/backend/app/controllers/wiki_pages_controller.rb @@ -20,17 +20,18 @@ class WikiPagesController < ApplicationController diffs = Diff::LCS.sdiff(wiki_page_from.body, wiki_page_to.body) diff_json = diffs.map { |change| - case change.action - when ?= - { type: 'context', content: change.old_element } - when ?| - [{ type: 'removed', content: change.old_element }, - { type: 'added', content: change.new_element }] - when ?+ - { type: 'added', content: change.new_element } - when ?- - { type: 'removed', content: change.old_element } - end }.flatten.compact + case change.action + when ?= + { type: 'context', content: change.old_element } + when ?| + [{ type: 'removed', content: change.old_element }, + { type: 'added', content: change.new_element }] + when ?+ + { type: 'added', content: change.new_element } + when ?- + { type: 'removed', content: change.old_element } + end + }.flatten.compact render json: { wiki_page_id: wiki_page_from.id, title: wiki_page_from.title, @@ -54,14 +55,19 @@ class WikiPagesController < ApplicationController def update return head :unauthorized unless current_user - return head :forbidden unless ['admin', 'member'].include?(current_user.role) + return head :forbidden unless current_user.member? - wiki_page = WikiPage.find(params[:id]) - return head :not_found unless wiki_page + title = params[:title] + body = params[:body] + return head :unprocessable_entity if title.blank? || body.blank? + + wiki_page = WikiPage.find(params[:id]) + wiki_page.title = title wiki_page.updated_user = current_user - wiki_page.set_body params[:body], user: current_user + wiki_page.set_body(body, user: current_user) wiki_page.save! + head :ok end @@ -71,28 +77,34 @@ class WikiPagesController < ApplicationController q = WikiPage.all q = q.where('title LIKE ?', "%#{ WikiPage.sanitize_sql_like(title) }%") if title.present? - render json: q.limit(20).map { |page| - page.sha = nil - page } + render json: q.limit(20) end def changes id = params[:id] - log = id.present? ? wiki.page("#{ id }.md")&.versions : wiki.repo.log('main', nil, max_count: 50) + log = if id.present? + wiki.page("#{ id }.md")&.versions + else + wiki.repo.log('main', nil) + end return render json: [] unless log render json: log.map { |commit| - wiki_page = WikiPage.find(commit.message.split(' ')[1].to_i) - wiki_page.sha = commit.id - user = User.find(commit.author.name.to_i) - { sha: wiki_page.sha, - pred: wiki_page.pred, - succ: wiki_page.succ, - - wiki_page: wiki_page && { id: wiki_page.id, title: wiki_page.title }, - user: user && { id: user.id, name: user.name }, - change_type: commit.message.split(' ')[0].downcase[0...(-1)], - timestamp: commit.authored_date } } + wiki_page = WikiPage.find(commit.message.split(' ')[1].to_i) + wiki_page.sha = commit.id + + next nil if wiki_page.sha.blank? + + user = User.find(commit.author.name.to_i) + + { sha: wiki_page.sha, + pred: wiki_page.pred, + succ: wiki_page.succ, + wiki_page: wiki_page && { id: wiki_page.id, title: wiki_page.title }, + user: user && { id: user.id, name: user.name }, + change_type: commit.message.split(' ')[0].downcase[0...(-1)], + timestamp: commit.authored_date } + }.compact end private diff --git a/backend/app/models/post_tag.rb b/backend/app/models/post_tag.rb index 04b4f28..9dbd756 100644 --- a/backend/app/models/post_tag.rb +++ b/backend/app/models/post_tag.rb @@ -1,6 +1,6 @@ class PostTag < ApplicationRecord belongs_to :post - belongs_to :tag + belongs_to :tag, counter_cache: :post_count validates :post_id, presence: true validates :tag_id, presence: true diff --git a/backend/app/models/user.rb b/backend/app/models/user.rb index 51e3d3d..830d383 100644 --- a/backend/app/models/user.rb +++ b/backend/app/models/user.rb @@ -19,4 +19,12 @@ class User < ApplicationRecord def viewed? post user_post_views.exists? post_id: post.id end + + def member? + ['member', 'admin'].include?(role) + end + + def admin? + role == 'admin' + end end diff --git a/backend/app/models/wiki_page.rb b/backend/app/models/wiki_page.rb index 9b89049..2e57372 100644 --- a/backend/app/models/wiki_page.rb +++ b/backend/app/models/wiki_page.rb @@ -8,6 +8,11 @@ class WikiPage < ApplicationRecord validates :title, presence: true, length: { maximum: 255 }, uniqueness: true + def as_json options = { } + self.sha = nil + super options + end + def sha= val if val.present? @sha = val @@ -18,9 +23,16 @@ class WikiPage < ApplicationRecord end vers = @page.versions idx = vers.find_index { |ver| ver.id == @sha } - @pred = vers[idx + 1]&.id - @succ = idx.positive? ? vers[idx - 1].id : nil - @updated_at = vers[idx].authored_date + if idx + @pred = vers[idx + 1]&.id + @succ = idx.positive? ? vers[idx - 1].id : nil + @updated_at = vers[idx].authored_date + else + @sha = nil + @pred = nil + @succ = nil + @updated_at = nil + end @sha end diff --git a/backend/config/routes.rb b/backend/config/routes.rb index 859eccf..65af8fd 100644 --- a/backend/config/routes.rb +++ b/backend/config/routes.rb @@ -1,5 +1,6 @@ Rails.application.routes.draw do get 'tags/autocomplete', to: 'tags#autocomplete' + get 'tags/name/:name', to: 'tags#show_by_name' get 'posts/random', to: 'posts#random' post 'posts/:id/viewed', to: 'posts#viewed' delete 'posts/:id/viewed', to: 'posts#unviewed' diff --git a/backend/db/migrate/20250629140234_add_posts_count_to_tags.rb b/backend/db/migrate/20250629140234_add_posts_count_to_tags.rb new file mode 100644 index 0000000..eaedbf5 --- /dev/null +++ b/backend/db/migrate/20250629140234_add_posts_count_to_tags.rb @@ -0,0 +1,5 @@ +class AddPostsCountToTags < ActiveRecord::Migration[8.0] + def change + add_column :tags, :post_count, :integer, null: false, default: 0 + end +end diff --git a/frontend/src/components/TagSidebar.tsx b/frontend/src/components/TagSidebar.tsx index 52038c3..81c416b 100644 --- a/frontend/src/components/TagSidebar.tsx +++ b/frontend/src/components/TagSidebar.tsx @@ -1,4 +1,5 @@ import axios from 'axios' +import toCamel from 'camelcase-keys' import React, { useEffect, useState } from 'react' import { Link, useNavigate, useParams } from 'react-router-dom' @@ -18,11 +19,9 @@ export default ({ posts }: Props) => { const navigate = useNavigate () const [tags, setTags] = useState ({ }) - const [tagsCounts, setTagsCounts] = useState<{ [key: number]: number }> ({ }) useEffect (() => { const tagsTmp: TagByCategory = { } - const tagsCountsTmp: { [key: number]: number } = { } for (const post of posts) { for (const tag of post.tags) @@ -31,15 +30,11 @@ export default ({ posts }: Props) => { tagsTmp[tag.category] = [] if (!(tagsTmp[tag.category].map (t => t.id).includes (tag.id))) 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) - setTagsCounts (tagsCountsTmp) }, [posts]) return ( @@ -54,7 +49,7 @@ export default ({ posts }: Props) => { {tag.name} - {tagsCounts[tag.id]} + {tag.postCount} ))} ))} diff --git a/frontend/src/components/TopNav.tsx b/frontend/src/components/TopNav.tsx index 3da0091..c4f5988 100644 --- a/frontend/src/components/TopNav.tsx +++ b/frontend/src/components/TopNav.tsx @@ -1,11 +1,15 @@ +import axios from 'axios' +import toCamel from 'camelcase-keys' import React, { useState, useEffect } from 'react' import { Link, useLocation, useNavigate, useParams } from 'react-router-dom' -import SettingsDialogue from './SettingsDialogue' -import { Button } from './ui/button' -import { cn } from '@/lib/utils' + +import SettingsDialogue from '@/components/SettingsDialogue' +import { Button } from '@/components/ui/button' +import { API_BASE_URL } from '@/config' import { WikiIdBus } from '@/lib/eventBus/WikiIdBus' +import { cn } from '@/lib/utils' -import type { User } from '@/types' +import type { Tag, User, WikiPage } from '@/types' type Props = { user: User setUser: (user: User) => void } @@ -23,13 +27,14 @@ const TopNav: React.FC = ({ user, setUser }: Props) => { const [settingsVsbl, setSettingsVsbl] = useState (false) const [selectedMenu, setSelectedMenu] = useState (Menu.None) - const [wikiId, setWikiId] = useState (WikiIdBus.get ()) + const [wikiId, setWikiId] = useState (WikiIdBus.get ()) const [wikiSearch, setWikiSearch] = useState ('') const [activeIndex, setActiveIndex] = useState (-1) const [suggestions, setSuggestions] = useState ([]) const [suggestionsVsbl, setSuggestionsVsbl] = useState (false) const [tagSearch, setTagSearch] = useState ('') const [userSearch, setUserSearch] = useState ('') + const [postCount, setPostCount] = useState (null) const MyLink = ({ to, title, menu, base }: { to: string title: string @@ -93,7 +98,8 @@ const TopNav: React.FC = ({ user, setUser }: Props) => { } useEffect (() => { - WikiIdBus.subscribe (setWikiId) + const unsubscribe = WikiIdBus.subscribe (setWikiId) + return () => unsubscribe () }, []) useEffect (() => { @@ -109,6 +115,28 @@ const TopNav: React.FC = ({ user, setUser }: Props) => { setSelectedMenu (Menu.None) }, [location]) + useEffect (() => { + if (!(wikiId)) + return + + void ((async () => { + try + { + const { data: pageData } = await axios.get (`${ API_BASE_URL }/wiki/${ wikiId }`) + const wikiPage: WikiPage = toCamel (pageData, { deep: true }) + + const { data: tagData } = await axios.get (`${ API_BASE_URL }/tags/name/${ wikiPage.title }`) + const tag: Tag = toCamel (tagData, { deep: true }) + + setPostCount (tag.postCount) + } + catch + { + setPostCount (0) + } + }) ()) + }, [wikiId]) + return ( <>