Spike persisted filters
This commit is contained in:
@@ -2,24 +2,15 @@ class BubblesController < ApplicationController
|
||||
include BucketScoped
|
||||
|
||||
before_action :set_bubble, only: %i[ show edit update ]
|
||||
before_action :set_assignee_filters, :set_tag_filters, only: :index
|
||||
|
||||
def index
|
||||
@bubbles = @bucket.bubbles
|
||||
@bubbles = params.include?(:popped) ? @bubbles.popped : @bubbles.not_popped
|
||||
|
||||
if params[:term].present?
|
||||
@bubbles = @bubbles.mentioning(params[:term])
|
||||
end
|
||||
|
||||
if params[:tag_id]
|
||||
@tag = Current.account.tags.find(params[:tag_id])
|
||||
@bubbles = @bubbles.tagged_with(@tag)
|
||||
end
|
||||
|
||||
if params[:assignee_id]
|
||||
@assignee = @bucket.users.find(params[:assignee_id])
|
||||
@bubbles = @bubbles.assigned_to(@assignee)
|
||||
end
|
||||
@bubbles = @bubbles.ordered_by(params[:order_by] || Bubble.default_order_by)
|
||||
@bubbles = @bubbles.with_status(params[:status] || Bubble.default_status)
|
||||
@bubbles = @bubbles.tagged_with(@tag_filters) if @tag_filters
|
||||
@bubbles = @bubbles.assigned_to(@assignee_filters) if @assignee_filters
|
||||
@bubbles = @bubbles.mentioning(params[:term]) if params[:term]
|
||||
end
|
||||
|
||||
def new
|
||||
@@ -38,13 +29,27 @@ class BubblesController < ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
@bubble.update!(bubble_params)
|
||||
@bubble.update! bubble_params
|
||||
redirect_to bucket_bubble_url(@bucket, @bubble)
|
||||
end
|
||||
|
||||
private
|
||||
def set_bubble
|
||||
@bubble = @bucket.bubbles.find(params[:id])
|
||||
@bubble = @bucket.bubbles.find params[:id]
|
||||
end
|
||||
|
||||
def set_assignee_filters
|
||||
params[:assignee_ids] = nil if status_filter_param.unassigned?
|
||||
@assignee_filters = Current.account.users.where(id: params[:assignee_ids]) if params[:assignee_ids]
|
||||
end
|
||||
|
||||
def status_filter_param
|
||||
params.fetch(:status, "")&.inquiry
|
||||
end
|
||||
helper_method :status_filter_param
|
||||
|
||||
def set_tag_filters
|
||||
@tag_filters = Current.account.tags.where(id: params[:tag_ids]) if params[:tag_ids]
|
||||
end
|
||||
|
||||
def bubble_params
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
class Buckets::ViewsController < ApplicationController
|
||||
include BucketScoped
|
||||
|
||||
def create
|
||||
@bucket.views.create! filters: view_params.merge(assignee_ids:, tag_ids:).compact_blank
|
||||
redirect_back_or_to bucket_bubbles_path(@bucket), notice: "Filters saved"
|
||||
end
|
||||
|
||||
private
|
||||
def view_params
|
||||
params.require(:view).permit(:order_by, :status, :assignee_ids, :tag_ids)
|
||||
end
|
||||
|
||||
def assignee_ids
|
||||
view_params[:assignee_ids]&.split(",")
|
||||
end
|
||||
|
||||
def tag_ids
|
||||
view_params[:tag_ids]&.split(",")
|
||||
end
|
||||
end
|
||||
@@ -2,7 +2,7 @@ class BucketsController < ApplicationController
|
||||
before_action :set_bucket, except: %i[ index new create ]
|
||||
|
||||
def index
|
||||
@buckets = Current.user.buckets.all
|
||||
@buckets = (Current.user.buckets.all + Current.user.bucket_views.all).sort_by(&:updated_at)
|
||||
end
|
||||
|
||||
def new
|
||||
@@ -10,7 +10,7 @@ class BucketsController < ApplicationController
|
||||
end
|
||||
|
||||
def create
|
||||
@bucket = Current.account.buckets.create!(bucket_params)
|
||||
@bucket = Current.account.buckets.create! bucket_params
|
||||
redirect_to bucket_bubbles_url(@bucket)
|
||||
end
|
||||
|
||||
|
||||
@@ -20,8 +20,4 @@ module BubblesHelper
|
||||
|
||||
"--bubble-size: var(--bubble-size-#{rank});"
|
||||
end
|
||||
|
||||
def bubble_filter_params
|
||||
@bubble_filter_params ||= params.permit(:popped, :term, :tag_id, :assignee_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
module FiltersHelper
|
||||
def main_filter_text
|
||||
(Bucket::View::ORDERS[params[:order_by]] || Bucket::View::STATUSES[params[:status]] || Bubble.default_order_by.humanize).upcase_first
|
||||
end
|
||||
|
||||
def tag_filter_text
|
||||
if @tag_filters
|
||||
@tag_filters.map(&:hashtag).to_choice_sentence
|
||||
else
|
||||
"any tag"
|
||||
end
|
||||
end
|
||||
|
||||
def assignee_filter_text
|
||||
if @assignee_filters
|
||||
"assigned to #{@assignee_filters.map(&:name).to_choice_sentence}"
|
||||
else
|
||||
"assigned to anyone"
|
||||
end
|
||||
end
|
||||
|
||||
# `#bubble_filter_params` is memoized to avoid spam in logs about unpermitted params
|
||||
def bubble_filter_params
|
||||
@bubble_filter_params ||= params.permit :order_by, :status, :term, :assignee_ids, :tag_ids, assignee_ids: [], tag_ids: []
|
||||
end
|
||||
end
|
||||
+20
-7
@@ -1,4 +1,5 @@
|
||||
class Bubble < ApplicationRecord
|
||||
include ::Searchable
|
||||
include Assignable, Boostable, Colored, Commentable, Eventable, Poppable, Searchable, Taggable, Threaded
|
||||
|
||||
belongs_to :bucket
|
||||
@@ -10,16 +11,28 @@ class Bubble < ApplicationRecord
|
||||
|
||||
before_save :set_default_title
|
||||
|
||||
scope :reverse_chronologically, -> { order(created_at: :desc, id: :desc) }
|
||||
scope :reverse_chronologically, -> { order created_at: :desc, id: :desc }
|
||||
scope :chronologically, -> { order created_at: :asc, id: :asc }
|
||||
scope :ordered_by_activity, -> { left_joins(:comments).group(:id).order(Arel.sql("COUNT(comments.id) + boost_count DESC")) }
|
||||
scope :with_status, ->(status) { public_send status.presence_in(%w[ popped not_popped unassigned ]) }
|
||||
scope :ordered_by, ->(order) do
|
||||
case order
|
||||
when "most_active" then ordered_by_activity
|
||||
when "most_discussed" then ordered_by_comments
|
||||
when "most_boosted" then ordered_by_boosts
|
||||
when "newest" then reverse_chronologically
|
||||
when "oldest" then chronologically
|
||||
end
|
||||
end
|
||||
|
||||
scope :mentioning, ->(query) do
|
||||
bubbles = search(query).select(:id).to_sql
|
||||
comments = Comment.search(query).select(:bubble_id).to_sql
|
||||
class << self
|
||||
def default_order_by
|
||||
"most_active"
|
||||
end
|
||||
|
||||
left_joins(:comments)
|
||||
.where("bubbles.id in (#{bubbles}) or comments.bubble_id in (#{comments})")
|
||||
.distinct
|
||||
def default_status
|
||||
"not_popped"
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -7,8 +7,8 @@ module Bubble::Assignable
|
||||
has_many :assignees, through: :assignments
|
||||
has_many :assigners, through: :assignments
|
||||
|
||||
scope :assigned_to, ->(user) { joins(:assignments).where(assignments: { assignee: user }) }
|
||||
scope :assigned_by, ->(user) { joins(:assignments).where(assignments: { assigner: user }) }
|
||||
scope :unassigned, -> { where.missing :assignments }
|
||||
scope :assigned_to, ->(users) { joins(:assignments).where(assignments: { assignee: users }) }
|
||||
end
|
||||
|
||||
def assign(users, assigner: Current.user)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
module Bubble::Boostable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
scope :ordered_by_boosts, -> { order boost_count: :desc }
|
||||
end
|
||||
|
||||
def boost!
|
||||
transaction do
|
||||
increment! :boost_count
|
||||
|
||||
@@ -3,6 +3,8 @@ module Bubble::Commentable
|
||||
|
||||
included do
|
||||
has_many :comments, dependent: :destroy
|
||||
|
||||
scope :ordered_by_comments, -> { left_joins(:comments).group(:id).order("COUNT(comments.id) DESC") }
|
||||
end
|
||||
|
||||
def comment!(body)
|
||||
|
||||
@@ -4,8 +4,8 @@ module Bubble::Poppable
|
||||
included do
|
||||
has_one :pop, dependent: :destroy
|
||||
|
||||
scope :popped, -> { joins(:pop) }
|
||||
scope :not_popped, -> { where.missing(:pop) }
|
||||
scope :popped, -> { joins(:pop) }
|
||||
scope :not_popped, -> { where.missing(:pop) }
|
||||
end
|
||||
|
||||
def popped?
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
module Bubble::Searchable
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
scope :mentioning, ->(query) do
|
||||
bubbles = search(query).select(:id).to_sql
|
||||
comments = Comment.search(query).select(:bubble_id).to_sql
|
||||
|
||||
left_joins(:comments)
|
||||
.where("bubbles.id in (#{bubbles}) or comments.bubble_id in (#{comments})")
|
||||
.distinct
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -5,6 +5,6 @@ module Bubble::Taggable
|
||||
has_many :taggings, dependent: :destroy
|
||||
has_many :tags, through: :taggings
|
||||
|
||||
scope :tagged_with, ->(tag) { joins(:taggings).where(taggings: { tag: tag }) }
|
||||
scope :tagged_with, ->(tags) { joins(:taggings).where(taggings: { tag: tags }) }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
class Bucket < ApplicationRecord
|
||||
include Accessible
|
||||
include Accessible, Views
|
||||
|
||||
belongs_to :account
|
||||
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
class Bucket::View < ApplicationRecord
|
||||
FILTERS = [ :assignee_ids, :order_by, :status, :tag_ids ]
|
||||
|
||||
include Summarized, Assignees, Tags
|
||||
|
||||
belongs_to :creator, class_name: "User", default: -> { Current.user }
|
||||
belongs_to :bucket
|
||||
|
||||
has_one :account, through: :creator
|
||||
|
||||
store_accessor :filters, *FILTERS
|
||||
|
||||
validate :must_have_filters, :must_not_be_the_default_view
|
||||
|
||||
def to_bucket_params
|
||||
filters.compact_blank
|
||||
end
|
||||
|
||||
private
|
||||
ORDERS = {
|
||||
"most_active" => "most active",
|
||||
"most_discussed" => "most discussed",
|
||||
"most_boosted" => "most boosted",
|
||||
"newest" => "newest",
|
||||
"oldest" => "oldest" }
|
||||
STATUSES = {
|
||||
"unassigned" => "unassigned",
|
||||
"popped" => "popped" }
|
||||
|
||||
def must_have_filters
|
||||
errors.add(:base, "must have filters") if filters.values.all?(&:blank?)
|
||||
end
|
||||
|
||||
def must_not_be_the_default_view
|
||||
errors.add(:base, "must be different than the default view") if filters.compact_blank == { "order_by" => "most_active" }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
module Bucket::View::Assignees
|
||||
private
|
||||
def assignee_names
|
||||
assignees.map &:name
|
||||
end
|
||||
|
||||
def assignees
|
||||
@assignees ||= account.users.where id: assignee_ids
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
module Bucket::View::Summarized
|
||||
def summary
|
||||
[ order_by_summary, status_summary, assignee_summary, tag_summary ].compact.to_sentence.upcase_first
|
||||
end
|
||||
|
||||
private
|
||||
delegate :to_sentence, to: "ApplicationController.helpers", private: true
|
||||
|
||||
def order_by_summary
|
||||
Bucket::View::ORDERS.fetch order_by, nil
|
||||
end
|
||||
|
||||
def status_summary
|
||||
Bucket::View::STATUSES.fetch status, nil
|
||||
end
|
||||
|
||||
def assignee_summary
|
||||
"assigned to #{assignee_names.to_choice_sentence}" if assignee_names.any?
|
||||
end
|
||||
|
||||
def tag_summary
|
||||
"tagged #{tag_names.to_choice_sentence}" if tag_names.any?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
module Bucket::View::Tags
|
||||
private
|
||||
def tag_names
|
||||
tags.map &:hashtag
|
||||
end
|
||||
|
||||
def tags
|
||||
@tags ||= account.tags.where id: tag_ids
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
module Bucket::Views
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :views, dependent: :delete_all
|
||||
end
|
||||
end
|
||||
@@ -6,5 +6,7 @@ class Event < ApplicationRecord
|
||||
belongs_to :creator, class_name: "User"
|
||||
belongs_to :bubble
|
||||
|
||||
has_one :account, through: :creator
|
||||
|
||||
scope :threadable, -> { where action: THREADABLE_ACTIONS }
|
||||
end
|
||||
|
||||
@@ -11,6 +11,6 @@ module Event::Assignments
|
||||
|
||||
private
|
||||
def assignees
|
||||
@assignees ||= creator.account.users.find assignee_ids
|
||||
@assignees ||= account.users.where id: assignee_ids
|
||||
end
|
||||
end
|
||||
|
||||
@@ -3,4 +3,8 @@ class Tag < ApplicationRecord
|
||||
|
||||
has_many :taggings, dependent: :destroy
|
||||
has_many :bubbles, through: :taggings
|
||||
|
||||
def hashtag
|
||||
"#" + title
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class User < ApplicationRecord
|
||||
include Viewer
|
||||
|
||||
belongs_to :account
|
||||
|
||||
has_many :sessions, dependent: :destroy
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module User::Viewer
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
has_many :bucket_views, class_name: "Bucket::View", foreign_key: :creator_id, dependent: :destroy
|
||||
end
|
||||
end
|
||||
@@ -1,21 +1,18 @@
|
||||
<h1 class="txt-large">
|
||||
<div class="filter align-center gap-half" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
|
||||
<button class="btn btn--plain filter__button" data-action="click->dialog#toggle" data-dialog-modal-value="true">
|
||||
<% if params[:popped] %>
|
||||
Popped
|
||||
<% else %>
|
||||
Most active
|
||||
<% end %>
|
||||
<%= main_filter_text %>
|
||||
</button>
|
||||
|
||||
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
|
||||
<menu class="filter__menu unpad margin-none">
|
||||
<li><%= link_to "Most active", bucket_bubbles_path(bucket, bubble_filter_params.without(:popped)), class: "filter__button" %></li>
|
||||
<li><a href="#" class="filter__button">Most discussed</a></li>
|
||||
<li><a href="#" class="filter__button">Most boosted</a></li>
|
||||
<li><a href="#" class="filter__button">Newest</a></li>
|
||||
<li><a href="#" class="filter__button">Oldest</a></li>
|
||||
<li><%= link_to "Popped", bucket_bubbles_path(bucket, bubble_filter_params.merge(popped: true)), class: "filter__button" %></li>
|
||||
<% Bucket::View::ORDERS.each do |key, value| %>
|
||||
<li><%= link_to value.upcase_first, bucket_bubbles_path(bucket, bubble_filter_params.merge(order_by: key, status: nil)), class: "filter__button" %></li>
|
||||
<% end %>
|
||||
|
||||
<% Bucket::View::STATUSES.each do |key, value| %>
|
||||
<li><%= link_to value.upcase_first, bucket_bubbles_path(bucket, bubble_filter_params.merge(order_by: nil, status: key)), class: "filter__button" %></li>
|
||||
<% end %>
|
||||
</menu>
|
||||
</dialog>
|
||||
</div>
|
||||
@@ -30,7 +27,7 @@
|
||||
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
|
||||
<menu class="filter__menu unpad margin-none">
|
||||
<% Current.user.buckets.order(:name).each do |bucket| %>
|
||||
<li><%= link_to bucket.name, bucket_bubbles_path(bucket), class: "filter__button" %></li>
|
||||
<li><%= link_to bucket.name, bucket_bubbles_path(bucket, bubble_filter_params), class: "filter__button" %></li>
|
||||
<% end %>
|
||||
</menu>
|
||||
</dialog>
|
||||
@@ -43,49 +40,61 @@
|
||||
</h1>
|
||||
|
||||
<h2 class="txt-medium flex align-center justify-center gap-half">
|
||||
<%= tag ? "Tagged" : "with" %>
|
||||
<%= @tag_filters ? "Tagged" : "with" %>
|
||||
|
||||
<div class="filter align-center gap-half" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
|
||||
<button class="btn btn--plain filter__button" data-action="click->dialog#toggle" data-dialog-modal-value="true">
|
||||
<%= tag&.title || "any tag" %>
|
||||
<%= tag_filter_text %>
|
||||
</button>
|
||||
|
||||
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
|
||||
<menu class="filter__menu unpad margin-none">
|
||||
<% bucket.tags.order(:title).each do |tag| %>
|
||||
<li><%= link_to tag.title, bucket_bubbles_path(bucket, bubble_filter_params.merge(tag_id: tag.id)) %></li>
|
||||
<li><%= link_to tag.title, bucket_bubbles_path(bucket, bubble_filter_params.merge(tag_ids: [ tag.id ])) %></li>
|
||||
<% end %>
|
||||
</menu>
|
||||
</dialog>
|
||||
|
||||
<% if tag %>
|
||||
<%= link_to bucket_bubbles_path(bucket, bubble_filter_params.without(:tag_id)), class: "btn", style: "font-size: 0.4em;" do %>
|
||||
<% if @tag_filters %>
|
||||
<%= link_to bucket_bubbles_path(bucket, bubble_filter_params.without(:tag_ids)), class: "btn", style: "font-size: 0.4em;" do %>
|
||||
<%= image_tag "remove.svg", aria: { hidden: true }, size: 24 %>
|
||||
<span class="for-screen-reader">Clear</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
and
|
||||
<% unless status_filter_param.unassigned? %>
|
||||
and
|
||||
|
||||
<div class="filter align-center gap-half" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
|
||||
<button class="btn btn--plain filter__button" data-action="click->dialog#toggle" data-dialog-modal-value="true">
|
||||
assigned to <%= assignee&.name || "anyone" %>
|
||||
</button>
|
||||
<div class="filter align-center gap-half" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
|
||||
<button class="btn btn--plain filter__button" data-action="click->dialog#toggle" data-dialog-modal-value="true">
|
||||
<%= assignee_filter_text %>
|
||||
</button>
|
||||
|
||||
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
|
||||
<menu class="filter__menu unpad margin-none">
|
||||
<% bucket.users.active.order(:name).each do |user| %>
|
||||
<li><%= link_to user.name, bucket_bubbles_path(bucket, bubble_filter_params.merge(assignee_id: user.id)) %></li>
|
||||
<dialog class="filter__popup panel fill-white shadow" data-dialog-target="dialog">
|
||||
<menu class="filter__menu unpad margin-none">
|
||||
<% bucket.users.active.order(:name).each do |user| %>
|
||||
<li><%= link_to user.name, bucket_bubbles_path(bucket, bubble_filter_params.merge(assignee_ids: [ user.id ])) %></li>
|
||||
<% end %>
|
||||
</menu>
|
||||
</dialog>
|
||||
|
||||
<% if @assignee_filters %>
|
||||
<%= link_to bucket_bubbles_path(bucket, bubble_filter_params.without(:assignee_ids)), class: "btn", style: "font-size: 0.4em;" do %>
|
||||
<%= image_tag "remove.svg", aria: { hidden: true }, size: 24 %>
|
||||
<span class="for-screen-reader">Clear</span>
|
||||
<% end %>
|
||||
</menu>
|
||||
</dialog>
|
||||
|
||||
<% if assignee %>
|
||||
<%= link_to bucket_bubbles_path(bucket, bubble_filter_params.without(:assignee_id)), class: "btn", style: "font-size: 0.4em;" do %>
|
||||
<%= image_tag "remove.svg", aria: { hidden: true }, size: 24 %>
|
||||
<span class="for-screen-reader">Clear</span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= button_to bucket_views_path(bucket), class: "btn", style: "font-size: 0.4em;" do %>
|
||||
<%= hidden_field_tag :order_by, params[:order_by], name: "view[order_by]" %>
|
||||
<%= hidden_field_tag :status, params[:status], name: "view[status]" %>
|
||||
<%= hidden_field_tag :assignee_ids, Array(params[:assignee_ids]).join(","), name: "view[assignee_ids]" %>
|
||||
<%= hidden_field_tag :tag_ids, Array(params[:tag_ids]).join(","), name: "view[tag_ids]" %>
|
||||
|
||||
<%= image_tag "add.svg", aria: { hidden: true }, size: 24 %>
|
||||
<span class="for-screen-reader">Save filters</span>
|
||||
<% end %>
|
||||
</h2>
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
<% end %>
|
||||
|
||||
<section class="windshield flex-inline flex-wrap gap justify-center align-end" style="view-transition-name: windshield_<%= @bucket.id %>">
|
||||
<% if @bubbles.ordered_by_activity.any? %>
|
||||
<%= render partial: "bubbles/bubble", collection: @bubbles.ordered_by_activity.limit(10) %>
|
||||
<% if @bubbles.any? %>
|
||||
<%= render partial: "bubbles/bubble", collection: @bubbles.limit(10) %>
|
||||
<% else %>
|
||||
<p><strong>Nothing here.</strong></p>
|
||||
<% end %>
|
||||
@@ -37,6 +37,6 @@
|
||||
</header>
|
||||
|
||||
<ul class="unpad margin-none flex flex-column txt-align-start center">
|
||||
<%= render partial: "bubbles/list/bubble", collection: @bubbles.ordered_by_activity %>
|
||||
<%= render partial: "bubbles/list/bubble", collection: @bubbles %>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
<small class="flex align-center gap flex-item-no-shrink">
|
||||
<% bubble.tags.each do |tag| %>
|
||||
<%= link_to "##{tag.title}", bucket_bubbles_path(bubble.bucket, tag_id: tag.id), style: "color: #{bubble.color}" %>
|
||||
<%= link_to "##{tag.title}", bucket_bubbles_path(bubble.bucket, tag_ids: [ tag.id ]), style: "color: #{bubble.color}" %>
|
||||
<% end %>
|
||||
|
||||
<%= tag.time bubble.created_at, class: "txt-nowrap flex-item-justify-end" do %>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<li class="bucket flex flex-column txt-align-center max-width position-relative">
|
||||
<%= link_to bucket_bubbles_path(bucket), class: "windshield__container flex justify-center align-center position-relative" do %>
|
||||
<div class="windshield bucket__windshield flex flex-wrap gap justify-center align-end" style="view-transition-name: windshield_<%= bucket.id %>">
|
||||
<% bucket.bubbles.not_popped.ordered_by_activity.limit(10).each do |bubble| %>
|
||||
<div class="bubble" style="--bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %> <%= bubble_size(bubble) %>">
|
||||
<svg class="bubble__svg" style="fill: <%= bubble.color %>; stroke: <%= bubble.color %>;" viewBox="0 0 990 990" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m0 0h990v990h-990z" fill="none" stroke="none" />
|
||||
<path d="m391.65 879.47c-110.52-15.95-212.21-91.86-255.92-191.23-66.78-143.65-41.62-347.61 48.08-481.17 368.33-516.3 1252.97 520.2 451.03 660.78-44.07 8.84-88.98 13.49-133.01 15.68-36.69 2-73.37 1.91-109.99-4.03z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex align-center justify-center flex-column gap-half center">
|
||||
<strong><%= link_to bucket.name, bucket_bubbles_path(bucket), class: "txt-x-large txt-ink" %></strong>
|
||||
</div>
|
||||
</li>
|
||||
@@ -0,0 +1,21 @@
|
||||
<li class="bucket flex flex-column txt-align-center max-width position-relative">
|
||||
<%= link_to bucket_bubbles_path(view.bucket, **view.to_bucket_params), class: "windshield__container flex justify-center align-center position-relative" do %>
|
||||
<div class="windshield bucket__windshield flex flex-wrap gap justify-center align-end" style="view-transition-name: windshield_<%= view.bucket.id %>">
|
||||
<% view.bucket.bubbles.not_popped.ordered_by_activity.limit(10).each do |bubble| %>
|
||||
<div class="bubble" style="--bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %> <%= bubble_size(bubble) %>">
|
||||
<svg class="bubble__svg" style="fill: <%= bubble.color %>; stroke: <%= bubble.color %>;" viewBox="0 0 990 990" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m0 0h990v990h-990z" fill="none" stroke="none" />
|
||||
<path d="m391.65 879.47c-110.52-15.95-212.21-91.86-255.92-191.23-66.78-143.65-41.62-347.61 48.08-481.17 368.33-516.3 1252.97 520.2 451.03 660.78-44.07 8.84-88.98 13.49-133.01 15.68-36.69 2-73.37 1.91-109.99-4.03z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="flex align-center justify-center flex-column center">
|
||||
<%= link_to bucket_bubbles_path(view.bucket, **view.to_bucket_params), class: "txt-ink" do %>
|
||||
<strong class="txt-x-large"><%= view.bucket.name %></strong>
|
||||
<p class="margin-none"><%= view.summary %></p>
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
@@ -16,28 +16,10 @@
|
||||
|
||||
<menu class="buckets margin-none unpad align-center justify-center flex flex-wrap gap">
|
||||
<% @buckets.each do |bucket| %>
|
||||
<li class="bucket flex flex-column txt-align-center max-width position-relative">
|
||||
<%= link_to bucket_bubbles_path(bucket), class: "windshield__container flex justify-center align-center position-relative" do %>
|
||||
<div class="windshield bucket__windshield flex flex-wrap gap justify-center align-end" style="view-transition-name: windshield_<%= bucket.id %>">
|
||||
|
||||
<% bucket.bubbles.not_popped.ordered_by_activity.limit(10).each do |bubble| %>
|
||||
<div class="bubble" style="--bubble-color: <%= bubble.color %>; <%= bubble_rotation(bubble) %> <%= bubble_size(bubble) %>">
|
||||
<svg class="bubble__svg" style="fill: <%= bubble.color %>; stroke: <%= bubble.color %>;" viewBox="0 0 990 990" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m0 0h990v990h-990z" fill="none" stroke="none" />
|
||||
<path d="m391.65 879.47c-110.52-15.95-212.21-91.86-255.92-191.23-66.78-143.65-41.62-347.61 48.08-481.17 368.33-516.3 1252.97 520.2 451.03 660.78-44.07 8.84-88.98 13.49-133.01 15.68-36.69 2-73.37 1.91-109.99-4.03z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex align-center justify-center flex-column gap-half center">
|
||||
<strong><%= link_to bucket.name, bucket_bubbles_path(bucket), class: "txt-x-large txt-ink" %></strong>
|
||||
|
||||
<%= link_to edit_bucket_path(bucket), class: "btn txt-small", hidden: true do %>
|
||||
<%= image_tag "pencil.svg", aria: { hidden: true }, size: 24 %>
|
||||
<span class="for-screen-reader">Edit bucket</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</li>
|
||||
<% if bucket.is_a? Bucket %>
|
||||
<%= render partial: "buckets/bucket", locals: { bucket: bucket } %>
|
||||
<% elsif bucket.is_a? Bucket::View %>
|
||||
<%= render partial: "buckets/view", locals: { view: bucket } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</menu>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<strong><%= Current.user.name %></strong>
|
||||
</div>
|
||||
<div class="comment__body txt-align-start margin-block-start-half">
|
||||
<%= form_with model: Comment.new, url: bucket_bubble_comments_path(bubble.bucket, bubble), class: "flex flex-column gap full-width" do |form| %>
|
||||
<%= form_with model: Comment.new, url: bucket_bubble_comments_path(bubble.bucket, bubble), class: "flex flex-column gap full-width", data: { controller: "form", action: "keydown.meta+enter->form#submit" } do |form| %>
|
||||
<%= form.text_area :body, class: "input", required: true, rows: 4,
|
||||
placeholder: (bubble.comments.empty? && bubble.creator == Current.user) ? "Add some notes…" : "Type your comment…" %>
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
%w[ rails_ext ].each do |extensions_dir|
|
||||
Dir["#{Rails.root}/lib/#{extensions_dir}/*"].each { |path| require "#{extensions_dir}/#{File.basename(path)}" }
|
||||
end
|
||||
+10
-11
@@ -2,31 +2,30 @@ Rails.application.routes.draw do
|
||||
root "buckets#index"
|
||||
|
||||
resource :first_run
|
||||
|
||||
resource :session
|
||||
|
||||
resource :account do
|
||||
scope module: "accounts" do
|
||||
scope module: :accounts do
|
||||
resource :join_code
|
||||
|
||||
resources :users
|
||||
end
|
||||
end
|
||||
|
||||
get "join/:join_code", to: "users#new", as: :join
|
||||
post "join/:join_code", to: "users#create"
|
||||
|
||||
resources :users do
|
||||
scope module: "users" do
|
||||
scope module: :users do
|
||||
resource :avatar
|
||||
end
|
||||
end
|
||||
|
||||
resources :buckets do
|
||||
resource :access, controller: "buckets/accesses"
|
||||
resources :tags, only: :index
|
||||
|
||||
scope module: :buckets do
|
||||
resources :views
|
||||
end
|
||||
|
||||
resources :bubbles do
|
||||
scope module: "bubbles" do
|
||||
scope module: :bubbles do
|
||||
resource :image
|
||||
resource :pop
|
||||
end
|
||||
@@ -36,9 +35,9 @@ Rails.application.routes.draw do
|
||||
resources :comments
|
||||
resources :tags, shallow: true
|
||||
end
|
||||
|
||||
resources :tags, only: :index
|
||||
end
|
||||
|
||||
get "join/:join_code", to: "users#new", as: :join
|
||||
post "join/:join_code", to: "users#create"
|
||||
get "up", to: "rails/health#show", as: :rails_health_check
|
||||
end
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
class CreateViews < ActiveRecord::Migration[8.0]
|
||||
def change
|
||||
create_table :bucket_views do |t|
|
||||
t.references :creator, null: false
|
||||
t.references :bucket, null: false
|
||||
t.json :filters, default: {}, null: false
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
||||
Generated
+11
-1
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[8.0].define(version: 2024_10_09_211414) do
|
||||
ActiveRecord::Schema[8.0].define(version: 2024_10_15_224007) do
|
||||
create_table "accesses", force: :cascade do |t|
|
||||
t.integer "bucket_id", null: false
|
||||
t.integer "user_id", null: false
|
||||
@@ -80,6 +80,16 @@ ActiveRecord::Schema[8.0].define(version: 2024_10_09_211414) do
|
||||
t.index ["bucket_id"], name: "index_bubbles_on_bucket_id"
|
||||
end
|
||||
|
||||
create_table "bucket_views", force: :cascade do |t|
|
||||
t.integer "creator_id", null: false
|
||||
t.integer "bucket_id", null: false
|
||||
t.json "filters", default: {}, null: false
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["bucket_id"], name: "index_bucket_views_on_bucket_id"
|
||||
t.index ["creator_id"], name: "index_bucket_views_on_creator_id"
|
||||
end
|
||||
|
||||
create_table "buckets", force: :cascade do |t|
|
||||
t.integer "account_id", null: false
|
||||
t.integer "creator_id", null: false
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
class Array
|
||||
def to_choice_sentence
|
||||
to_sentence two_words_connector: " or ", last_word_connector: ", or "
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user