From 83197ced56e31ef65d1de67e21c484fed3089ad5 Mon Sep 17 00:00:00 2001 From: Andrew Brown Date: Mon, 23 Jun 2025 21:51:26 -0700 Subject: [PATCH] add new tags page to profiles --- CLAUDE.md | 131 +++++++++++ app/controllers/users_controller.rb | 132 ++++++++++- .../display/attribute_value/_tags.html.erb | 4 +- app/views/content/list/_cards.html.erb | 8 +- app/views/content/list/_dense_cards.html.erb | 4 +- .../content/list/_document_table.html.erb | 10 +- app/views/content/list/_list.html.erb | 4 +- app/views/users/profile/_tags.html.erb | 17 ++ app/views/users/show.html.erb | 78 +++---- app/views/users/tag.html.erb | 208 ++++++++++++++++++ config/routes.rb | 1 + 11 files changed, 551 insertions(+), 46 deletions(-) create mode 100644 CLAUDE.md create mode 100644 app/views/users/profile/_tags.html.erb create mode 100644 app/views/users/tag.html.erb diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..81c2f117 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,131 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Common Commands + +### Setup and Installation +```bash +# Install dependencies +bundle install + +# Install JavaScript dependencies +yarn install + +# Setup database +rake db:setup # Creates and seeds the database +rake db:migrate # Apply all pending migrations +``` + +### Development +```bash +# Start the Rails server +rails server +# Or +rails s + +# Start Sidekiq for background jobs +bundle exec sidekiq + +# Start the development server with both web and worker processes +RAILS_GROUPS=web,worker rails server +``` + +### Testing +```bash +# Run all tests +rails test + +# Run a specific test file +rails test path/to/test.rb + +# Run specific test +rails test path/to/test.rb:LINE_NUMBER +``` + +### Database Operations +```bash +# Reset database (CAUTION: Destroys all data) +rake db:reset + +# Migrate database +rake db:migrate + +# Rollback last migration +rake db:rollback +``` + +### Asset Management +```bash +# Compile assets +rails assets:precompile +``` + +### Deployment +```bash +# Run in production mode +RAILS_ENV=production rails server +``` + +## Architecture Overview + +Notebook.ai is a Rails application for writers and roleplayers to create and manage fictional universes and their components. The application follows standard Rails MVC architecture with some specific design patterns. + +### Key Concepts + +1. **Content Types**: The application revolves around different content types like Universe, Character, Location, etc. Each represents a different entity within a user's fictional world. + +2. **Content Pages**: All content types inherit from a shared ContentPage concern, which provides common functionality like attributes, privacy settings, and image uploads. + +3. **Universes**: The top-level organizational unit. Users create universes and add various content types within them. + +4. **Attributes System**: The application uses a flexible attributes system to store custom fields for each content type, allowing for extensibility without schema changes. + +5. **Privacy & Sharing**: Content can be private, public, or shared with specific collaborators. + +### Core Components + +#### Content Structure +- **Universe**: The top-level container for all world-building elements +- **Character**, **Location**, **Item**: Core content types that are available to all users +- **Premium Content Types**: Many additional content types (Creature, Planet, Religion, etc.) available to premium users + +#### Key Modules and Concerns +- `BelongsToUniverse`: Associates content with universes +- `IsContentPage`: Provides shared content page functionality +- `HasAttributes`: Handles dynamic attributes for content types +- `HasPrivacy`: Manages content privacy settings +- `HasImageUploads`: Handles image attachment functionality +- `ContentPage`: Base behavior for all content pages + +#### Background Processing +- Sidekiq is used for background job processing +- Document analysis, exports, and other intensive tasks run asynchronously + +#### Data Flow +1. Users create universes to contain their fictional worlds +2. Within universes, users create various content types (characters, locations, etc.) +3. Content types can be linked together with relationships (e.g., a character can be linked to locations) +4. Users can collaborate on universes, allowing multiple people to contribute to the same world + +### Directory Structure + +Beyond the standard Rails structure, notable directories include: +- `app/models/page_types/`: Contains all content type models +- `app/models/page_groupers/`: Contains relationship models between content types +- `config/attributes/`: Configuration for content type attributes +- `app/authorizers/`: Authorization logic for content access + +### Key Files +- `config/initializers/content_types.rb`: Defines all available content types +- `app/models/content_page.rb`: Base functionality for all content pages +- `app/controllers/content_controller.rb`: Base controller for all content types + +## Content Type System + +The application uses a content type system with these types of pages: +- Universe (top-level container) +- Character, Location, Item (core types) +- Many premium content types like Creature, Planet, Religion, etc. + +Creating a new content type requires following the process in `docs/content_types.md`. \ No newline at end of file diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 3d7ec93d..2d994640 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,5 +1,5 @@ class UsersController < ApplicationController - before_action :set_user, only: [:show, :followers, :following] + before_action :set_user, only: [:show, :followers, :following, :tag] def index redirect_to new_session_path(User) @@ -15,6 +15,9 @@ class UsersController < ApplicationController @content = @user.public_content.select { |type, list| list.any? } @tabs = @content.keys + + # Get popular tags for this user's public content + @popular_tags = get_popular_public_tags_for_user(@user) @favorite_content = @user.favorite_page_type? ? @user.send(@user.favorite_page_type.downcase.pluralize).is_public : [] @stream = @user.recent_content_list(limit: 20) @@ -91,6 +94,88 @@ class UsersController < ApplicationController def following end + + def tag + @tag_slug = params[:tag_slug] + @tag = PageTag.find_by(user_id: @user.id, slug: @tag_slug) + + return redirect_to(profile_by_username_path(username: @user.username), notice: 'That tag does not exist.') if @tag.nil? + + # Find all public content with this tag + @tagged_content = [] + + # Go through each content type and find items with this tag + Rails.application.config.content_types[:all].each do |content_type| + content_pages = content_type.joins(:page_tags) + .where(privacy: 'public') + .where(user_id: @user.id) + .where(page_tags: { slug: @tag_slug }) + .order(:name) + + @tagged_content << { + type: content_type.name, + icon: content_type.icon, + color: content_type.color, + content: content_pages + } if content_pages.any? + end + + # Add documents and timelines if they have the tag + # Handle documents separately since they don't use the common content type structure + documents = Document.joins(:page_tags) + .where(privacy: 'public') + .where(user_id: @user.id) + .where(page_tags: { slug: @tag_slug }) + .order(:title) # Documents use 'title' instead of 'name' + + @tagged_content << { + type: 'Document', + icon: 'description', + color: 'blue', + content: documents + } if documents.any? + + # Handle timelines separately since they don't use the common content type structure + timelines = Timeline.joins(:page_tags) + .where(privacy: 'public') + .where(user_id: @user.id) + .where(page_tags: { slug: @tag_slug }) + .order(:name) + + @tagged_content << { + type: 'Timeline', + icon: 'timeline', + color: 'blue', + content: timelines + } if timelines.any? + + # Get images for content cards + @random_image_including_private_pool_cache = ImageUpload.where( + user_id: @user.id, + ).group_by { |image| [image.content_type, image.content_id] } + + # Collect all content IDs and types for fetching basil commissions + basil_entity_types = [] + basil_entity_ids = [] + + @tagged_content.each do |content_group| + content_group[:content].each do |content| + basil_entity_types << content.class.name + basil_entity_ids << content.id + end + end + + # Initialize @saved_basil_commissions if there are any content items + if basil_entity_types.any? + @saved_basil_commissions = BasilCommission.where( + entity_type: basil_entity_types, + entity_id: basil_entity_ids + ).where.not(saved_at: nil) + .group_by { |commission| [commission.entity_type, commission.entity_id] } + end + + @sidenav_expansion = 'community' + end private @@ -107,4 +192,49 @@ class UsersController < ApplicationController def user_params params.permit(:id, :username) end + + # Get most popular tags for a user's public content + def get_popular_public_tags_for_user(user, limit: 10) + # Find page tags attached to public content + public_page_tags = [] + + # For each content type, find public pages with tags + Rails.application.config.content_types[:all].each do |content_type| + # Skip if a user has no pages of this type + next unless user.respond_to?(content_type.name.downcase.pluralize) + + # Get all public pages of this content type + public_pages = user.send(content_type.name.downcase.pluralize).is_public + + # Skip if there are no public pages + next if public_pages.empty? + + # Find all page tags for these pages + tag_ids = PageTag.where(page_type: content_type.name, page_id: public_pages.pluck(:id)).pluck(:id) + public_page_tags.concat(tag_ids) if tag_ids.any? + end + + # Also include Document and Timeline tags if they're public + public_documents = user.documents.where(privacy: 'public') + if public_documents.any? + document_tag_ids = PageTag.where(page_type: 'Document', page_id: public_documents.pluck(:id)).pluck(:id) + public_page_tags.concat(document_tag_ids) if document_tag_ids.any? + end + + public_timelines = user.timelines.where(privacy: 'public') + if public_timelines.any? + timeline_tag_ids = PageTag.where(page_type: 'Timeline', page_id: public_timelines.pluck(:id)).pluck(:id) + public_page_tags.concat(timeline_tag_ids) if timeline_tag_ids.any? + end + + # If we have tags, find the most popular ones + return [] if public_page_tags.empty? + + # Get the actual tags + PageTag.where(id: public_page_tags) + .select('tag, slug, COUNT(*) as usage_count') + .group(:tag, :slug) + .order('usage_count DESC') + .limit(limit) + end end diff --git a/app/views/content/display/attribute_value/_tags.html.erb b/app/views/content/display/attribute_value/_tags.html.erb index b0b68770..09fd1ce1 100644 --- a/app/views/content/display/attribute_value/_tags.html.erb +++ b/app/views/content/display/attribute_value/_tags.html.erb @@ -15,7 +15,9 @@ <% end %> <% else %> - + <%= link_to user_tag_path(username: content.user.username, tag_slug: PageTagService.slug_for(tag)) do %> + + <% end %> <% end %> <% end %> diff --git a/app/views/content/list/_cards.html.erb b/app/views/content/list/_cards.html.erb index 8e70fb34..7f61aa0b 100644 --- a/app/views/content/list/_cards.html.erb +++ b/app/views/content/list/_cards.html.erb @@ -33,7 +33,9 @@ <% end %> <% else %> - + <%= link_to user_tag_path(username: content.user.username, tag_slug: tag.slug) do %> + + <% end %> <% end %> <% end %>

@@ -80,7 +82,9 @@ <% end %> <% else %> - + <%= link_to user_tag_path(username: content.user.username, tag_slug: tag.slug) do %> + + <% end %> <% end %> <% end %>

diff --git a/app/views/content/list/_dense_cards.html.erb b/app/views/content/list/_dense_cards.html.erb index b67a5f71..42d8f1ee 100644 --- a/app/views/content/list/_dense_cards.html.erb +++ b/app/views/content/list/_dense_cards.html.erb @@ -31,7 +31,9 @@ <% end %> <% else %> - + <%= link_to user_tag_path(username: content.user.username, tag_slug: tag.slug) do %> + + <% end %> <% end %> <% end %>

diff --git a/app/views/content/list/_document_table.html.erb b/app/views/content/list/_document_table.html.erb index 71411a28..9b6faa2d 100644 --- a/app/views/content/list/_document_table.html.erb +++ b/app/views/content/list/_document_table.html.erb @@ -61,7 +61,15 @@ <% if page_tags.any? %>

<% page_tags.each do |tag| %> - + <% if user_signed_in? && document.user == current_user %> + <%= link_to params.permit(:tag).merge({ tag: tag.slug }) do %> + + <% end %> + <% else %> + <%= link_to user_tag_path(username: document.user.username, tag_slug: tag.slug) do %> + + <% end %> + <% end %> <% end %>

<% end %> diff --git a/app/views/content/list/_list.html.erb b/app/views/content/list/_list.html.erb index 27447b8f..0b16808f 100644 --- a/app/views/content/list/_list.html.erb +++ b/app/views/content/list/_list.html.erb @@ -61,7 +61,9 @@ <% end %> <% else %> - + <%= link_to user_tag_path(username: content.user.username, tag_slug: tag.slug) do %> + + <% end %> <% end %> <% end %> <% end %> diff --git a/app/views/users/profile/_tags.html.erb b/app/views/users/profile/_tags.html.erb new file mode 100644 index 00000000..c73d6b95 --- /dev/null +++ b/app/views/users/profile/_tags.html.erb @@ -0,0 +1,17 @@ +<% if @popular_tags.any? %> +
+
+
+ Tags +
+
+
+ <% @popular_tags.each do |tag| %> + <%= link_to user_tag_path(username: @user.username, tag_slug: tag.slug) do %> + + <% end %> + <% end %> +
+
+
+<% end %> \ No newline at end of file diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 72c131cd..8d0881a7 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -39,9 +39,9 @@