diff --git a/.annotaterb.yml b/.annotaterb.yml new file mode 100644 index 00000000..89f547ae --- /dev/null +++ b/.annotaterb.yml @@ -0,0 +1,64 @@ +--- +:position: before +:position_in_additional_file_patterns: before +:position_in_class: before +:position_in_factory: before +:position_in_fixture: before +:position_in_routes: before +:position_in_serializer: before +:position_in_test: before +:classified_sort: true +:exclude_controllers: true +:exclude_factories: false +:exclude_fixtures: false +:exclude_helpers: true +:exclude_scaffolds: true +:exclude_serializers: false +:exclude_sti_subclasses: false +:exclude_tests: false +:force: false +:format_markdown: false +:format_rdoc: false +:format_yard: false +:frozen: false +:grouped_polymorphic: false +:ignore_model_sub_dir: false +:ignore_unknown_models: false +:include_version: false +:show_check_constraints: false +:show_complete_foreign_keys: false +:show_foreign_keys: true +:show_indexes: true +:show_indexes_include: false +:simple_indexes: false +:sort: false +:timestamp: false +:trace: false +:with_comment: true +:with_column_comments: true +:with_table_comments: true +:position_of_column_comment: :with_name +:active_admin: false +:command: +:debug: false +:hide_default_column_types: '' +:hide_limit_column_types: '' +:timestamp_columns: +- created_at +- updated_at +:ignore_columns: +:ignore_routes: +:models: true +:routes: false +:skip_on_db_migrate: false +:target_action: :do_annotations +:wrapper: +:wrapper_close: +:wrapper_open: +:classes_default_to_s: [] +:additional_file_patterns: [] +:model_dir: +- app/models +:require: [] +:root_dir: +- '' diff --git a/Gemfile b/Gemfile index c0dbfdfc..4680373d 100644 --- a/Gemfile +++ b/Gemfile @@ -71,7 +71,7 @@ group :development, :test do end group :development do - gem 'annotate' # Annotate models with schema + gem 'annotaterb' # Annotate models with schema (Rails 8 compatible fork) gem 'brakeman' # Static analysis security vulnerability scanner for Ruby on Rails applications gem 'letter_opener' # Preview emails in the browser instead of sending them gem 'web-console' # Rails console for the browser diff --git a/Gemfile.lock b/Gemfile.lock index fa844181..08858961 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -74,9 +74,9 @@ GEM uri (>= 0.13.1) addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) - annotate (2.6.5) - activerecord (>= 2.3.0) - rake (>= 0.8.7) + annotaterb (4.20.0) + activerecord (>= 6.0.0) + activesupport (>= 6.0.0) ast (2.4.2) autoprefixer-rails (10.4.16.0) execjs (~> 2) @@ -451,7 +451,7 @@ PLATFORMS x86_64-linux DEPENDENCIES - annotate + annotaterb autoprefixer-rails bootsnap bootstrap (~> 5.2) diff --git a/Procfile.dev b/Procfile.dev index 21e70575..a14581f3 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1 +1,2 @@ web: bin/rails server -p 3000 +ngrok: ngrok http 3000 --log=stdout diff --git a/README.md b/README.md index 6f155e92..45fca11c 100644 --- a/README.md +++ b/README.md @@ -12,8 +12,8 @@ These instructions will get you a copy of the project up and running on your loc Ensure that you have the following installed on your local machine: -* [Ruby](https://www.ruby-lang.org/en/documentation/installation/) - 3.1.2 -* [Rails](https://guides.rubyonrails.org/v5.0/getting_started.html) - 7.0.6 +* [Ruby](https://www.ruby-lang.org/en/documentation/installation/) - 3.4.4 +* [Rails](https://guides.rubyonrails.org/v5.0/getting_started.html) - 8.0.3 * PostgreSQL ### Installation @@ -62,6 +62,36 @@ rspec # run all tests rspec spec/requests/users_spec.rb # run a specific test ``` +### Development Server + +**Option 1: Using Foreman (recommended for production-like environment)** + +Start both Rails server and ngrok simultaneously: + +```bash +bin/dev +``` + +This will start: +- Rails server on `http://localhost:3000` +- ngrok tunnel (URL will be shown in the logs) + +**Option 2: Manual setup (recommended for debugging with binding.pry)** + +Start Rails server and ngrok separately: + +**Terminal 1 - Rails Server:** +```bash +rails s +``` + +**Terminal 2 - ngrok (for mobile testing):** +```bash +ngrok http 3000 +``` + +The ngrok URL will be displayed in Terminal 2 under "Forwarding" (e.g., `https://abc123.ngrok-free.app`). Use this URL to test the app on your mobile device. + ### Services - Poetry Service: Fetches a random poem to display on the page. diff --git a/app/assets/stylesheets/components/_map.scss b/app/assets/stylesheets/components/_map.scss index 87e5c5fa..38ff30ac 100644 --- a/app/assets/stylesheets/components/_map.scss +++ b/app/assets/stylesheets/components/_map.scss @@ -1,17 +1,7 @@ .rounded-map { - border-radius: 15px; /* Or use a SASS variable $border-radius if it's defined in your environment */ - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); /* Example shadow, adjust as needed */ + border-radius: $border-radius; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); overflow: hidden; - margin: 5px auto; - padding: 10px; /* Example padding, adjust using $space-xs if needed */ - - /* Default to smaller size */ - width: 340px !important; - height: 300px; - - /* Larger devices */ - @media (min-width: 768px) { - width: 100% !important; - height: 90vh; - } + width: 100%; + height: 600px; } diff --git a/app/assets/stylesheets/components/_navbar.scss b/app/assets/stylesheets/components/_navbar.scss index b98f7b51..f4f536c0 100644 --- a/app/assets/stylesheets/components/_navbar.scss +++ b/app/assets/stylesheets/components/_navbar.scss @@ -5,7 +5,8 @@ } .navbar .navbar-brand img { - width: 40px; + height: 50px; + width: auto; } .offcanvas { diff --git a/app/assets/stylesheets/components/_turbo_progress_bar.scss b/app/assets/stylesheets/components/_turbo_progress_bar.scss index b4027274..99de6cb7 100644 --- a/app/assets/stylesheets/components/_turbo_progress_bar.scss +++ b/app/assets/stylesheets/components/_turbo_progress_bar.scss @@ -1,3 +1,3 @@ .turbo-progress-bar { - background: linear-gradient(to right, $color-primary, $color-primary-rotate); + background: linear-gradient(to right, $soft-purple, $pink); } diff --git a/app/assets/stylesheets/config/_reset.scss b/app/assets/stylesheets/config/_reset.scss index 16ceed97..fc664b76 100644 --- a/app/assets/stylesheets/config/_reset.scss +++ b/app/assets/stylesheets/config/_reset.scss @@ -13,13 +13,22 @@ html { overflow-y: scroll; + overflow-x: hidden; height: 100%; + height: 100dvh; /* Modern dynamic viewport height */ + height: -webkit-fill-available; /* Fallback for older Safari */ + background-color: $off-white; } body { display: flex; flex-direction: column; min-height: 100%; + min-height: 100vh; /* Fallback for older browsers */ + min-height: 100dvh; /* Modern dynamic viewport height */ + min-height: -webkit-fill-available; /* Fallback for older Safari */ + overflow-x: hidden; + width: 100%; background-color: $color-background; color: $color-text-body; diff --git a/app/controllers/friendships_controller.rb b/app/controllers/friendships_controller.rb index fc9b0c4b..d00a6bda 100644 --- a/app/controllers/friendships_controller.rb +++ b/app/controllers/friendships_controller.rb @@ -12,26 +12,26 @@ def index def create @friendship = current_user.friendships.build(friend_id: params[:friend_id]) flash[:notice] = if @friendship.save - 'Friend request sent. 👻' + t('friendships.created') else - 'Unable to send friend request. 🤔' + t('friendships.create_failed') end - redirect_to users_path + redirect_to friends_path end def update flash[:notice] = if @friendship.update(accepted: true) - 'Friend request accepted. 🫱🏻‍🫲🏾' + t('friendships.accepted') else - 'Unable to accept friend request. 🙈' + t('friendships.accept_failed') end - redirect_to users_path + redirect_to friends_path end def destroy @friendship.destroy - flash[:notice] = 'Friendship removed. 😭' - redirect_to users_path + flash[:notice] = t('friendships.destroyed') + redirect_to friends_path end private @@ -41,7 +41,7 @@ def destroy # which could otherwise happen via ID guessing (e.g. /friendships/42) def set_friendship @friendship = Friendship.where(id: params[:id]) - .where('user_id = ? OR friend_id = ?', current_user.id, current_user.id) + .where('user_id = :user_id OR friend_id = :user_id', user_id: current_user.id) .first end end diff --git a/app/controllers/happy_things_controller.rb b/app/controllers/happy_things_controller.rb index 24ef8498..79aeca30 100644 --- a/app/controllers/happy_things_controller.rb +++ b/app/controllers/happy_things_controller.rb @@ -101,7 +101,7 @@ def fetch_words_for_wordcloud end def fetch_visited_places - @visited_places_count = @visited_places_count = HappyThing.where(user_id: current_user.id).distinct.count(:place) + @visited_places_count = HappyThing.where(user_id: current_user.id).distinct.count(:place) end def fetch_label_count diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 7b5cab2d..c94978d0 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -5,20 +5,25 @@ class UsersController < ApplicationController helper FriendshipsHelper include WordAggregator + attr_reader :user + def index @users = fetch_users end def friends - @friends = fetch_users + @friends = current_user.friends + @pending_requests = current_user.pending_friends + @friend_requests = fetch_incoming_friend_requests + @users = fetch_users end def show @user = User.find(params[:id]) - @happy_count = happy_count(@user) - @words_for_wordcloud = words_for_wordcloud(@user) - @visited_places_count = visited_places_count(@user) - @markers = markers(@user) + @happy_count = happy_count(user) + @words_for_wordcloud = words_for_wordcloud(user) + @visited_places_count = visited_places_count(user) + @markers = markers(user) end def profile @@ -26,11 +31,19 @@ def profile fetch_words_for_wordcloud fetch_visited_places fetch_label_count - @markers = current_user.happy_things.geocoded.map { |ht| { lat: ht.latitude, lng: ht.longitude } } + @markers = fetch_markers_for_map end private + def fetch_incoming_friend_requests + User.joins(:friendships).where(friendships: + { + friend_id: current_user.id, + accepted: false + }) + end + def fetch_users if params[:query].present? User.search(params[:query]).all_except(current_user) @@ -67,7 +80,7 @@ def fetch_words_for_wordcloud end def fetch_visited_places - @visited_places_count = @visited_places_count = HappyThing.where(user_id: current_user.id).distinct.count(:place) + @visited_places_count = current_user.happy_things.distinct.count(:place) end def fetch_label_count @@ -75,4 +88,8 @@ def fetch_label_count @label_counts = {} end + + def fetch_markers_for_map + current_user.happy_things.geocoded.pluck(:latitude, :longitude).map { |lat, lng| { lat:, lng: } } + end end diff --git a/app/helpers/friendships_helper.rb b/app/helpers/friendships_helper.rb index 6618bb30..6d9fa198 100644 --- a/app/helpers/friendships_helper.rb +++ b/app/helpers/friendships_helper.rb @@ -2,11 +2,23 @@ module FriendshipsHelper def can_add_as_friend?(current_user, potential_friend) - return false if current_user.friends.include?(potential_friend) + !current_user.friends.include?(potential_friend) && + !current_user.friendships.exists?(friend_id: potential_friend.id) + end + + def filter_users_by_query(users, query) + return users if query.blank? + + users.select { |user| matches_query?(user, query.downcase) } + end - sent_request = current_user.friendships.find_by(friend_id: potential_friend.id) - received_request = current_user.received_friend_requests.find_by(user_id: potential_friend.id) + private - !(sent_request || received_request) + # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + def matches_query?(user, query_downcase) + [user.first_name, user.last_name, user.username, user.email] + .compact + .any? { |field| field.downcase.include?(query_downcase) } end + # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity end diff --git a/app/models/comment.rb b/app/models/comment.rb index bd5c7b10..70226a5e 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -5,11 +5,21 @@ # Table name: comments # # id :bigint not null, primary key -# user_id :bigint not null -# happy_thing_id :bigint not null # content :text # created_at :datetime not null # updated_at :datetime not null +# happy_thing_id :bigint not null +# user_id :bigint not null +# +# Indexes +# +# index_comments_on_happy_thing_id (happy_thing_id) +# index_comments_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (happy_thing_id => happy_things.id) +# fk_rails_... (user_id => users.id) # class Comment < ApplicationRecord belongs_to :user diff --git a/app/models/friendship.rb b/app/models/friendship.rb index 8b5c369e..edde8f72 100644 --- a/app/models/friendship.rb +++ b/app/models/friendship.rb @@ -5,13 +5,24 @@ # Table name: friendships # # id :bigint not null, primary key -# user_id :bigint -# friend_id :bigint # accepted :boolean default(FALSE) # created_at :datetime not null # updated_at :datetime not null +# friend_id :bigint +# user_id :bigint +# +# Indexes # -# Friendship Model which makes sure a friendship is always either false or true (accepted) +# index_friendships_on_friend_id (friend_id) +# index_friendships_on_user_id (user_id) +# index_friendships_on_user_id_and_friend_id (user_id,friend_id) UNIQUE +# +# Foreign Keys +# +# fk_rails_... (friend_id => users.id) +# fk_rails_... (user_id => users.id) +# +# Friendship Model with bidirectional records for simplified queries class Friendship < ApplicationRecord belongs_to :user belongs_to :friend, class_name: 'User' @@ -20,4 +31,38 @@ class Friendship < ApplicationRecord scope :pending, -> { where(accepted: false) } validates :accepted, inclusion: { in: [true, false] } + + after_create :create_inverse_friendship + after_update :update_inverse_friendship, if: :saved_change_to_accepted? + after_destroy :destroy_inverse_friendship + + private + + def create_inverse_friendship + return if inverse_friendship_exists? + + self.class.create!( + user_id: friend_id, + friend_id: user_id, + accepted: accepted + ) + end + + def update_inverse_friendship + inverse = find_inverse_friendship + inverse&.update(accepted: accepted) + end + + def destroy_inverse_friendship + inverse = find_inverse_friendship + inverse&.destroy + end + + def find_inverse_friendship + self.class.find_by(user_id: friend_id, friend_id: user_id) + end + + def inverse_friendship_exists? + self.class.exists?(user_id: friend_id, friend_id: user_id) + end end diff --git a/app/models/group.rb b/app/models/group.rb index 6c76bb3f..e35f9ce4 100644 --- a/app/models/group.rb +++ b/app/models/group.rb @@ -6,9 +6,17 @@ # # id :bigint not null, primary key # name :string -# user_id :bigint not null # created_at :datetime not null # updated_at :datetime not null +# user_id :bigint not null +# +# Indexes +# +# index_groups_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) # class Group < ApplicationRecord belongs_to :user diff --git a/app/models/group_membership.rb b/app/models/group_membership.rb index 62228a5b..642eadca 100644 --- a/app/models/group_membership.rb +++ b/app/models/group_membership.rb @@ -5,10 +5,20 @@ # Table name: group_memberships # # id :bigint not null, primary key -# group_id :bigint not null -# friend_id :bigint not null # created_at :datetime not null # updated_at :datetime not null +# friend_id :bigint not null +# group_id :bigint not null +# +# Indexes +# +# index_group_memberships_on_friend_id (friend_id) +# index_group_memberships_on_group_id (group_id) +# +# Foreign Keys +# +# fk_rails_... (friend_id => users.id) +# fk_rails_... (group_id => groups.id) # class GroupMembership < ApplicationRecord belongs_to :group diff --git a/app/models/happy_thing.rb b/app/models/happy_thing.rb index 49212a4d..fe30962a 100644 --- a/app/models/happy_thing.rb +++ b/app/models/happy_thing.rb @@ -5,18 +5,30 @@ # Table name: happy_things # # id :bigint not null, primary key -# title :string not null # body :text +# latitude :float +# longitude :float +# place :string +# share_location :boolean +# start_time :datetime # status :integer -# user_id :bigint not null +# title :string not null +# visibility :string default("public") # created_at :datetime not null # updated_at :datetime not null -# start_time :datetime -# place :string -# latitude :float -# longitude :float # category_id :bigint -# share_location :boolean +# user_id :bigint not null +# +# Indexes +# +# index_happy_things_on_category_id (category_id) +# index_happy_things_on_user_id (user_id) +# index_happy_things_on_visibility (visibility) +# +# Foreign Keys +# +# fk_rails_... (category_id => categories.id) +# fk_rails_... (user_id => users.id) # class HappyThing < ApplicationRecord geocoded_by :place @@ -33,7 +45,7 @@ class HappyThing < ApplicationRecord validates :title, presence: true before_validation :set_default_category, on: :create - after_validation :geocode, if: :will_save_change_to_place? + after_validation :geocode, if: -> { will_save_change_to_place? && !Rails.env.test? && geocoding_enabled? } before_create :add_date_time_to_happy_thing, unless: :start_time_present? after_create :check_happy_things_count @@ -67,8 +79,14 @@ def check_happy_things_count end def notify_friends_about_happy_things - user.friends_and_friends_who_added_me.each do |friend| + user.all_friends.each do |friend| UserMailer.happy_things_notification(friend).deliver_later end end + + def geocoding_enabled? + return false if Rails.env.development? && ENV['GEOCODER_API_KEY'].blank? + + true + end end diff --git a/app/models/happy_thing_group_share.rb b/app/models/happy_thing_group_share.rb index e3f986c8..81bebabb 100644 --- a/app/models/happy_thing_group_share.rb +++ b/app/models/happy_thing_group_share.rb @@ -5,10 +5,20 @@ # Table name: happy_thing_group_shares # # id :bigint not null, primary key -# happy_thing_id :bigint not null -# group_id :bigint not null # created_at :datetime not null # updated_at :datetime not null +# group_id :bigint not null +# happy_thing_id :bigint not null +# +# Indexes +# +# index_happy_thing_group_shares_on_group_id (group_id) +# index_happy_thing_group_shares_on_happy_thing_id (happy_thing_id) +# +# Foreign Keys +# +# fk_rails_... (group_id => groups.id) +# fk_rails_... (happy_thing_id => happy_things.id) # class HappyThingGroupShare < ApplicationRecord belongs_to :happy_thing diff --git a/app/models/happy_thing_user_share.rb b/app/models/happy_thing_user_share.rb index 46c06a05..e1e29eb2 100644 --- a/app/models/happy_thing_user_share.rb +++ b/app/models/happy_thing_user_share.rb @@ -5,10 +5,20 @@ # Table name: happy_thing_user_shares # # id :bigint not null, primary key -# happy_thing_id :bigint not null -# friend_id :bigint not null # created_at :datetime not null # updated_at :datetime not null +# friend_id :bigint not null +# happy_thing_id :bigint not null +# +# Indexes +# +# index_happy_thing_user_shares_on_friend_id (friend_id) +# index_happy_thing_user_shares_on_happy_thing_id (happy_thing_id) +# +# Foreign Keys +# +# fk_rails_... (friend_id => users.id) +# fk_rails_... (happy_thing_id => happy_things.id) # class HappyThingUserShare < ApplicationRecord belongs_to :happy_thing diff --git a/app/models/user.rb b/app/models/user.rb index ea6e4cce..3aba1a1e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,30 +1,36 @@ # frozen_string_literal: true - +# # == Schema Information # # Table name: users # # id :bigint not null, primary key -# first_name :string -# last_name :string +# confirmation_sent_at :datetime +# confirmation_token :string +# confirmed_at :datetime # email :string default(""), not null +# email_opt_in :boolean default(FALSE) +# emoji :string # encrypted_password :string default(""), not null -# reset_password_token :string -# reset_password_sent_at :datetime +# first_name :string +# last_name :string +# location_opt_in :boolean default(FALSE) +# provider :string # remember_created_at :datetime +# reset_password_sent_at :datetime +# reset_password_token :string # sign_in_count :integer default(0), not null +# uid :string +# unconfirmed_email :string +# username :string # created_at :datetime not null # updated_at :datetime not null -# emoji :string -# email_opt_in :boolean default(FALSE) -# location_opt_in :boolean default(FALSE) -# username :string -# confirmation_token :string -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string -# provider :string -# uid :string +# +# Indexes +# +# index_users_on_confirmation_token (confirmation_token) UNIQUE +# index_users_on_email (email) UNIQUE +# index_users_on_reset_password_token (reset_password_token) UNIQUE # class User < ApplicationRecord # rubocop:disable Metrics/ClassLength PASSWORD_REGEX = /\A @@ -70,13 +76,10 @@ class User < ApplicationRecord # rubocop:disable Metrics/ClassLength has_many :group_memberships, foreign_key: :friend_id, dependent: :destroy has_many :groups_as_member, through: :group_memberships, source: :group - # Friendships + # Friendships - Bidirectional: each friendship exists as two records has_many :friendships, dependent: :destroy - has_many :received_friend_requests, class_name: 'Friendship', foreign_key: 'friend_id', dependent: :destroy has_many :friends, -> { where(friendships: { accepted: true }) }, through: :friendships, source: :friend - has_many :friends_who_added_me, lambda { - where(friendships: { accepted: true }) - }, through: :received_friend_requests, source: :user + has_many :pending_friends, -> { where(friendships: { accepted: false }) }, through: :friendships, source: :friend has_one_attached :avatar @@ -85,21 +88,13 @@ def self.search(query) query: "%#{query}%") end - def friends_and_friends_who_added_me_ids - friends.pluck(:id) + friends_who_added_me.pluck(:id) + def friend_ids + friends.pluck(:id) end - def all_friends - friends + friends_who_added_me - end - - def accepted_friends - friendships.where(status: :accepted) - end - - def pending_friends - friendships.pending + received_friend_requests.pending - end + # Aliases for backward compatibility + alias_method :friends_and_friends_who_added_me_ids, :friend_ids + alias_method :all_friends, :friends def happy_streak return 0 if happy_things.empty? @@ -110,10 +105,6 @@ def happy_streak calculate_streak(dates) end - def friends_and_friends_who_added_me - User.where(id: friends_and_friends_who_added_me_ids) - end - def self.from_omniauth(auth) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength # Returning user signing in via OAuth user = where(provider: auth.provider, uid: auth.uid).first diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index b3731188..4d4a9bb8 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -6,9 +6,17 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> - <%# PWA %> + <%# PWA - iOS %> + + + + + <%# PWA - Android %> + + + <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> diff --git a/app/views/users/friends.html.erb b/app/views/users/friends.html.erb index 03936b27..b927f4fe 100644 --- a/app/views/users/friends.html.erb +++ b/app/views/users/friends.html.erb @@ -1,121 +1,150 @@ -
- <%= link_to sanitize("←"), root_path, class: "btn button--primary mb-3" %> -

Add or Search Friends

+
+

<%= t('friends.page_title') %>

- <%= form_with(url: users_path, method: :get, local: true, class: 'form-inline mb-3') do %> + <%= form_with(url: friends_path, method: :get, local: true, class: 'form-inline mb-3') do %>
<%= text_field_tag :query, params[:query], - placeholder: "Search for names, usernames, or emails", - class: "form-control w-100 mr-2" %> - <%= submit_tag "🔍", class: "btn button--primary" %> + placeholder: t('friends.search_placeholder'), + class: "form-control w-100 mr-2", + style: "height: 50px;" %> + <%= submit_tag "🔍", class: "btn button--primary", style: "width: 50px; height: 50px; padding: 0; font-size: 1.25rem;" %>
<% end %> - -

My Friends

- <% current_user.all_friends.each do |friend| %> - <%= link_to user_path(friend), class: "text-decoration-none" do %> -
- <%= cl_image_tag friend.avatar.key, - class: "w-100 h-100 pb-3", - alt: "Friend Avatar", - transformation: [ - { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, - { radius: :max } - ] unless friend.avatar.key.nil? %> -

<%= friend.first_name %>

-
- <% end %> - <% end %> + + <% friends_list = filter_users_by_query(@friends, params[:query]) %> + <% if friends_list.any? %> +

<%= t('friends.my_friends') %>

- -

Pending Requests You Sent

- <% current_user.friendships.pending.each do |friendship| %> - <% friend = friendship.friend %> - <%= link_to user_path(friend), class: "text-decoration-none" do %> -
- <%= cl_image_tag friend.avatar.key, - style: "width: 50px; height: 100%;", - class: "pb-3", - alt: "Friend Avatar", - transformation: [ - { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, - { radius: :max } - ] unless friend.avatar.key.nil? %> -

<%= friend.first_name %>

- <%= button_to "⤬ Cancel", friendship_path(friendship), method: :delete, class: "btn button--primary" %> -
+ <% friends_list.each do |friend| %> + <%= link_to user_path(friend), class: "text-decoration-none" do %> +
+
+ <%= friend.emoji %> +
+

<%= friend.first_name %>

+
+ <% end %> <% end %> <% end %> - -

Friend Requests You Received

- <% current_user.received_friend_requests.pending.each do |friendship| %> - <% friend = friendship.user %> + + <% pending_requests = filter_users_by_query(@pending_requests, params[:query]) %> + <% if pending_requests.any? %> +

<%= t('friends.pending_sent') %>

-
-
- <%= link_to user_path(friend), class: "text-decoration-none d-inline-flex align-items-center" do %> + <% pending_requests.each do |friend| %> +
+ <%= link_to user_path(friend), class: "text-decoration-none d-flex align-items-center gap-3" do %> + <% if friend.emoji.present? %> +
+ <%= friend.emoji %> +
+ <% elsif friend.avatar.key.present? %> <%= cl_image_tag friend.avatar.key, - style: "width: 50px; height: 50px;", - class: "me-2 rounded-circle", - alt: "Friend Avatar", - transformation: [ - { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, - { radius: :max } - ] unless friend.avatar.key.nil? %> -

<%= friend.first_name %>

+ class: "rounded-circle", + style: "width: 60px; height: 60px; object-fit: cover; flex-shrink: 0;", + alt: "Friend Avatar", + transformation: [ + { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, + { radius: :max } + ] %> <% end %> -
+

<%= friend.first_name %>

+ <% end %> + <% friendship = current_user.friendships.find_by(friend_id: friend.id) %> + <%= button_to "X", friendship_path(friendship), method: :delete, class: "btn button--primary", style: "width: 50px; height: 50px; padding: 0; font-size: 1.25rem;" %> +
+ <% end %> + <% end %> -
- <%= button_to "✅ Accept", - friendship_path(friendship), - method: :patch, - data: { turbo: false }, - class: "btn button--primary mt-2 position-relative" %> -
+ + <% friend_requests = filter_users_by_query(@friend_requests, params[:query]) %> + <% if friend_requests.any? %> +

<%= t('friends.pending_received') %>

+ <% friend_requests.each do |friend| %> +
+ <%= link_to user_path(friend), class: "text-decoration-none d-flex align-items-center gap-3" do %> + <% if friend.emoji.present? %> +
+ <%= friend.emoji %> +
+ <% elsif friend.avatar.key.present? %> + <%= cl_image_tag friend.avatar.key, + class: "rounded-circle", + style: "width: 60px; height: 60px; object-fit: cover; flex-shrink: 0;", + alt: "Friend Avatar", + transformation: [ + { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, + { radius: :max } + ] %> + <% end %> +

<%= friend.first_name %>

+ <% end %> + <% friendship = Friendship.find_by(user_id: friend.id, friend_id: current_user.id) %> + <%= button_to "✓", + friendship_path(friendship), + method: :patch, + data: { turbo: false }, + class: "btn button--primary", + style: "width: 50px; height: 50px; padding: 0; font-size: 1.25rem;" %>
<% end %> + <% end %>
-

Users

- <% @friends.each do |friend| %> - <% next unless can_add_as_friend?(current_user, friend) %> - <% next if friend == current_user || current_user.friends.include?(friend) %> - - <% direct_friendship = current_user.friendships.find { |f| f.friend_id == friend.id } %> - <% inverse_friendship = friend.friendships.find { |f| f.user_id == current_user.id } %> + <% filtered_users = filter_users_by_query(@users, params[:query]).select { |user| can_add_as_friend?(current_user, user) } %> + <% if filtered_users.any? %> +

<%= t('friends.users') %>

+ <% filtered_users.each do |user| %> + <% direct_friendship = current_user.friendships.find { |f| f.friend_id == user.id } %> + <% inverse_friendship = user.friendships.find { |f| f.user_id == current_user.id } %> -
-
- <%= cl_image_tag friend.avatar.key, - style: "width: 50px; height: 100%;", - class: "pb-3 d-flex", - alt: "User Avatar", - transformation: [ - { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, - { radius: :max } - ] unless friend.avatar.key.nil? %> -
- -
-

<%= friend.first_name %>

-
- -
- <% if direct_friendship && !direct_friendship.accepted %> - <%= button_to "Cancel Request", friendship_path(direct_friendship), method: :delete, class: "btn button--primary" %> - <% elsif inverse_friendship && !inverse_friendship.accepted %> - <%= button_to "Accept Request", friendship_path(inverse_friendship), method: :patch, class: "btn button--primary" %> - <% elsif !direct_friendship && !inverse_friendship %> - <%= button_to "➕", friendships_path(friend_id: friend.id), method: :post, class: "btn button--primary" %> - <% end %> +
+
+ <% if user.emoji.present? %> +
+ <%= user.emoji %> +
+ <% elsif user.avatar.key.present? %> + <%= cl_image_tag user.avatar.key, + class: "rounded-circle", + style: "width: 60px; height: 60px; object-fit: cover; flex-shrink: 0;", + alt: "User Avatar", + transformation: [ + { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, + { radius: :max } + ] %> + <% end %> +

<%= user.first_name %>

+
+ +
+ <% if direct_friendship && !direct_friendship.accepted %> + <%= button_to "X", friendship_path(direct_friendship), method: :delete, class: "btn button--primary", style: "width: 50px; height: 50px; padding: 0; font-size: 1.25rem;" %> + <% elsif inverse_friendship && !inverse_friendship.accepted %> + <%= button_to "✓", friendship_path(inverse_friendship), method: :patch, class: "btn button--primary", style: "width: 50px; height: 50px; padding: 0; font-size: 1.25rem;" %> + <% elsif !direct_friendship && !inverse_friendship %> + <%= button_to "+", friendships_path(friend_id: user.id), method: :post, class: "btn button--primary", style: "width: 50px; height: 50px; padding: 0; font-size: 1.25rem;" %> + <% end %> +
-
+ <% end %> <% end %>
+ + + <% if params[:query].present? && friends_list.empty? && pending_requests.empty? && friend_requests.empty? && filtered_users.empty? %> +

+ <%= t('friends.no_results') %> "<%= params[:query] %>" +

+ <% end %> +
diff --git a/app/views/users/index.html.erb b/app/views/users/index.html.erb deleted file mode 100644 index 9a1d3d90..00000000 --- a/app/views/users/index.html.erb +++ /dev/null @@ -1,121 +0,0 @@ -
- <%= link_to sanitize("←"), root_path, class: "btn button--primary mb-3" %> -

Add or Search Friends

- - - <%= form_with(url: users_path, method: :get, local: true, class: 'form-inline mb-3') do %> -
- <%= text_field_tag :query, params[:query], - placeholder: "Search for names, usernames, or emails", - class: "form-control w-100 mr-2" %> - <%= submit_tag "🔍", class: "btn button--primary" %> -
- <% end %> - - -

My Friends

- <% current_user.all_friends.each do |friend| %> - <%= link_to user_path(friend), class: "text-decoration-none" do %> -
- <%= cl_image_tag friend.avatar.key, - class: "w-100 h-100 pb-3", - alt: "Friend Avatar", - transformation: [ - { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, - { radius: :max } - ] unless friend.avatar.key.nil? %> -

<%= friend.first_name %>

-
- <% end %> - <% end %> - - -

Pending Requests You Sent

- <% current_user.friendships.pending.each do |friendship| %> - <% friend = friendship.friend %> - <%= link_to user_path(friend), class: "text-decoration-none" do %> -
- <%= cl_image_tag friend.avatar.key, - style: "width: 50px; height: 100%;", - class: "pb-3", - alt: "Friend Avatar", - transformation: [ - { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, - { radius: :max } - ] unless friend.avatar.key.nil? %> -

<%= friend.first_name %>

- <%= button_to "⤬ Cancel", friendship_path(friendship), method: :delete, class: "btn button--primary" %> -
- <% end %> - <% end %> - - -

Friend Requests You Received

- <% current_user.received_friend_requests.pending.each do |friendship| %> - <% friend = friendship.user %> - -
-
- <%= link_to user_path(friend), class: "text-decoration-none d-inline-flex align-items-center" do %> - <%= cl_image_tag friend.avatar.key, - style: "width: 50px; height: 50px;", - class: "me-2 rounded-circle", - alt: "Friend Avatar", - transformation: [ - { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, - { radius: :max } - ] unless friend.avatar.key.nil? %> -

<%= friend.first_name %>

- <% end %> -
- -
- <%= button_to "✅ Accept", - friendship_path(friendship), - method: :patch, - data: { turbo: false }, - class: "btn button--primary mt-2 position-relative" %> -
-
- <% end %> - - -
-

Users

- <% @users.each do |user| %> - <% next unless can_add_as_friend?(current_user, user) %> - <% next if user == current_user || current_user.friends.include?(user) %> - - <% direct_friendship = current_user.friendships.find { |f| f.friend_id == user.id } %> - <% inverse_friendship = user.friendships.find { |f| f.user_id == current_user.id } %> - -
-
- <%= cl_image_tag user.avatar.key, - style: "width: 50px; height: 100%;", - class: "pb-3 d-flex", - alt: "User Avatar", - transformation: [ - { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, - { radius: :max } - ] unless user.avatar.key.nil? %> -
- -
-

<%= user.first_name %>

-
- -
- <% if direct_friendship && !direct_friendship.accepted %> - <%= button_to "Cancel Request", friendship_path(direct_friendship), method: :delete, class: "btn button--primary" %> - <% elsif inverse_friendship && !inverse_friendship.accepted %> - <%= button_to "Accept Request", friendship_path(inverse_friendship), method: :patch, class: "btn button--primary" %> - <% elsif !direct_friendship && !inverse_friendship %> - <%= button_to "➕", friendships_path(friend_id: user.id), method: :post, class: "btn button--primary" %> - <% end %> -
-
- - <% end %> -
-
diff --git a/app/views/users/profile.html.erb b/app/views/users/profile.html.erb index 29fda3ca..aeb62c06 100644 --- a/app/views/users/profile.html.erb +++ b/app/views/users/profile.html.erb @@ -1,25 +1,29 @@
- <%= link_to sanitize("←"), root_path, class: "btn button--primary mb-3" %> -

My Profile

- -
- <%= cl_image_tag current_user.avatar.key, - style: "border-radius: 50%; width: 200px; height: 100%;", - class: "pb-3", - alt: "User Avatar", - transformation: [ - { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, - { radius: :max } - ] unless current_user.avatar.key.nil? %> + +
+

<%= current_user.first_name %>

- -
-

<%= current_user.emoji %> <%= current_user.first_name %>

+
+ <% if current_user.emoji.present? %> +
+ <%= current_user.emoji %> +
+ <% elsif current_user.avatar.key.present? %> + <%= cl_image_tag current_user.avatar.key, + style: "border-radius: 50%; width: 200px; height: 100%;", + class: "pb-3", + alt: "User Avatar", + transformation: [ + { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, + { radius: :max } + ] %> + <% end %>
-
+

Happy Count

<%= @happy_count %> @@ -33,7 +37,8 @@ <% if @words_for_wordcloud.present? %>
<% @words_for_wordcloud.each do |word, frequency| %> - <%= content_tag :span, word.capitalize, style: "font-size: #{5 + frequency * 2}px;" %> + <% size = [[0.5 + frequency * 0.2, 0.75].max, 2.5].min %> + <%= content_tag :span, word.capitalize, style: "font-size: #{size}rem;" %> <% end %>
<% else %> diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 4e7fd5bc..823ce6e5 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -1,24 +1,28 @@
- <%= link_to sanitize("←"), users_path, class: "btn button--primary mb-3" %> -

<%= @user.first_name %>'s Profile

- -
- <%= cl_image_tag @user.avatar.key, - style: "border-radius: 50%; width: 200px; height: 100%;", - class: "pb-3", - alt: "User Avatar", - transformation: [ - { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, - { radius: :max } - ] unless @user.avatar.key.nil? %> +
+

<%= @user.first_name %>

-
-

<%= @user.emoji %> <%= @user.first_name %>

+
+ <% if @user.emoji.present? %> +
+ <%= @user.emoji %> +
+ <% elsif @user.avatar.key.present? %> + <%= cl_image_tag @user.avatar.key, + style: "border-radius: 50%; width: 200px; height: 100%;", + class: "pb-3", + alt: "User Avatar", + transformation: [ + { aspect_ratio: :"1.0", crop: :fill, gravity: :face }, + { radius: :max } + ] %> + <% end %>
-
+

Happy Count

<%= @happy_count %> @@ -48,16 +52,27 @@ <% if @markers %> -
+
+
+
<% end %> <% if @user == current_user %> <%= link_to "Edit Profile", edit_user_registration_path, class: "mb-5 btn nav-link bottom-icons" %> <%= button_to "Logout", destroy_user_session_path, method: :delete, class: "btn shadow mt-5" %> + <% else %> + <% friendship = current_user.friendships.accepted.find_by(friend_id: @user.id) %> + <% if friendship.present? %> +
+ <%= button_to "Remove Friend", friendship_path(friendship), method: :delete, + class: "btn shadow", + data: { confirm: "Are you sure you want to remove #{@user.first_name} as a friend?" } %> +
+ <% end %> <% end %>
diff --git a/config/environments/development.rb b/config/environments/development.rb index d4aa1308..530159ae 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -74,8 +74,11 @@ # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true - # Local Tunnel (Allow requests) - config.hosts.clear + # Allow ngrok and local connections + # Allows any ngrok subdomain for development + config.hosts << 'localhost' + config.hosts << /[a-z0-9-]+\.ngrok-free\.app/ + config.hosts << /[a-z0-9-]+\.ngrok\.io/ # Use an explicit host for mailer URLs. Rails.application.routes.default_url_options[:host] = 'localhost:3000' diff --git a/config/locales/de.yml b/config/locales/de.yml index 5964eb9c..94fabe2e 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -22,3 +22,19 @@ de: password: length: "muss zwischen 12 und 30 Zeichen lang sein" invalid: "muss mindestens einen Großbuchstaben, einen Kleinbuchstaben, eine Zahl und ein Sonderzeichen enthalten und darf keine Leerzeichen enthalten" + + friendships: + created: "Freundschaftsanfrage gesendet. 👻" + create_failed: "Freundschaftsanfrage konnte nicht gesendet werden. 🤔" + accepted: "Freundschaftsanfrage akzeptiert. 🫱🏻‍🫲🏾" + accept_failed: "Freundschaftsanfrage konnte nicht akzeptiert werden. 🙈" + destroyed: "Freundschaft entfernt. 😭" + + friends: + page_title: "Freundinnen hinzufügen oder suchen" + search_placeholder: "Nach Namen, Benutzernamen oder E-Mails suchen" + my_friends: "Meine Freundinnen" + pending_sent: "Gesendete Anfragen" + pending_received: "Empfangene Freundschaftsanfragen" + users: "Benutzerinnen" + no_results: "Keine Benutzerinnen gefunden für" diff --git a/config/locales/en.yml b/config/locales/en.yml index e1667382..e51a3e45 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -22,3 +22,19 @@ en: password: length: "must be between 12 and 30 characters long" invalid: "must include at least one uppercase, one lowercase, one number, and one special character, and cannot contain spaces" + + friendships: + created: "Friend request sent. 👻" + create_failed: "Unable to send friend request. 🤔" + accepted: "Friend request accepted. 🫱🏻‍🫲🏾" + accept_failed: "Unable to accept friend request. 🙈" + destroyed: "Friendship removed. 😭" + + friends: + page_title: "Add or Search Friends" + search_placeholder: "Search for names, usernames, or emails" + my_friends: "My Friends" + pending_sent: "Pending Requests You Sent" + pending_received: "Friend Requests You Received" + users: "Users" + no_results: "No users found matching" diff --git a/config/locales/sv.yml b/config/locales/sv.yml new file mode 100644 index 00000000..f6fa9bd1 --- /dev/null +++ b/config/locales/sv.yml @@ -0,0 +1,40 @@ +sv: + simple_form: + placeholders: + happy_thing: + title: Vad fick dig att le idag? + labels: + happy_thing: + title: Namn + + helpers: + submit: + happy_thing: + create: Skapa glad sak + update: Uppdatera glad sak + + errors: + models: + user: + first_name: + length: "måste vara mellan 3 och 30 tecken lång" + invalid: "kan inte innehålla länkar eller skript" + password: + length: "måste vara mellan 12 och 30 tecken lång" + invalid: "måste innehålla minst en stor bokstav, en liten bokstav, en siffra och ett specialtecken, och kan inte innehålla mellanslag" + + friendships: + created: "Vänförfrågan skickad. 👻" + create_failed: "Kunde inte skicka vänförfrågan. 🤔" + accepted: "Vänförfrågan accepterad. 🫱🏻‍🫲🏾" + accept_failed: "Kunde inte acceptera vänförfrågan. 🙈" + destroyed: "Vänskap borttagen. 😭" + + friends: + page_title: "Lägg till eller sök vänner" + search_placeholder: "Sök efter namn, användarnamn eller e-post" + my_friends: "Mina vänner" + pending_sent: "Skickade förfrågningar" + pending_received: "Mottagna vänförfrågningar" + users: "Användare" + no_results: "Inga användare hittades för" diff --git a/db/migrate/20251026140700_remove_duplicate_friendships_and_add_unique_index.rb b/db/migrate/20251026140700_remove_duplicate_friendships_and_add_unique_index.rb new file mode 100644 index 00000000..44c58435 --- /dev/null +++ b/db/migrate/20251026140700_remove_duplicate_friendships_and_add_unique_index.rb @@ -0,0 +1,23 @@ +class RemoveDuplicateFriendshipsAndAddUniqueIndex < ActiveRecord::Migration[8.0] + def up + # Remove bidirectional duplicates - keep only one direction per friendship + # For each pair of users, keep the friendship with the lower ID + execute <<-SQL + DELETE FROM friendships + WHERE id IN ( + SELECT f2.id + FROM friendships f1 + INNER JOIN friendships f2 ON f1.user_id = f2.friend_id AND f1.friend_id = f2.user_id + WHERE f1.id < f2.id + ) + SQL + + # Note: Unique index already exists in schema + # index_friendships_on_user_id_and_friend_id is already present + end + + def down + # Duplicates were removed, can't be restored + # Index already existed, won't be removed + end +end diff --git a/db/schema.rb b/db/schema.rb index 46377015..8d911c37 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -12,157 +10,170 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2025_06_18_210827) do +ActiveRecord::Schema[8.0].define(version: 2025_10_26_140700) do # These are extensions that must be enabled in order to support this database - enable_extension 'plpgsql' + enable_extension "pg_catalog.plpgsql" + + create_table "active_storage_attachments", force: :cascade do |t| + t.string "name", null: false + t.string "record_type", null: false + t.bigint "record_id", null: false + t.bigint "blob_id", null: false + t.datetime "created_at", null: false + t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id" + t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true + end - create_table 'active_storage_attachments', force: :cascade do |t| - t.string 'name', null: false - t.string 'record_type', null: false - t.bigint 'record_id', null: false - t.bigint 'blob_id', null: false - t.datetime 'created_at', null: false - t.index ['blob_id'], name: 'index_active_storage_attachments_on_blob_id' - t.index %w[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', - unique: true + create_table "active_storage_blobs", force: :cascade do |t| + t.string "key", null: false + t.string "filename", null: false + t.string "content_type" + t.text "metadata" + t.string "service_name", null: false + t.bigint "byte_size", null: false + t.string "checksum" + t.datetime "created_at", null: false + t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end - create_table 'active_storage_blobs', force: :cascade do |t| - t.string 'key', null: false - t.string 'filename', null: false - t.string 'content_type' - t.text 'metadata' - t.string 'service_name', null: false - t.bigint 'byte_size', null: false - t.string 'checksum' - t.datetime 'created_at', null: false - t.index ['key'], name: 'index_active_storage_blobs_on_key', unique: true + create_table "active_storage_variant_records", force: :cascade do |t| + t.bigint "blob_id", null: false + t.string "variation_digest", null: false + t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true end - create_table 'active_storage_variant_records', force: :cascade do |t| - t.bigint 'blob_id', null: false - t.string 'variation_digest', null: false - t.index %w[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true + create_table "categories", force: :cascade do |t| + t.string "name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - create_table 'categories', force: :cascade do |t| - t.string 'name' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false + create_table "comments", force: :cascade do |t| + t.bigint "user_id", null: false + t.bigint "happy_thing_id", null: false + t.text "content" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["happy_thing_id"], name: "index_comments_on_happy_thing_id" + t.index ["user_id"], name: "index_comments_on_user_id" end - create_table 'comments', force: :cascade do |t| - t.bigint 'user_id', null: false - t.bigint 'happy_thing_id', null: false - t.text 'content' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['happy_thing_id'], name: 'index_comments_on_happy_thing_id' - t.index ['user_id'], name: 'index_comments_on_user_id' + create_table "daily_happy_email_deliveries", force: :cascade do |t| + t.bigint "user_id", null: false + t.bigint "recipient_id", null: false + t.datetime "delivered_at", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["recipient_id"], name: "index_daily_happy_email_deliveries_on_recipient_id" + t.index ["user_id"], name: "index_daily_happy_email_deliveries_on_user_id" end - create_table 'friendships', force: :cascade do |t| - t.bigint 'user_id' - t.bigint 'friend_id' - t.boolean 'accepted', default: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['friend_id'], name: 'index_friendships_on_friend_id' - t.index %w[user_id friend_id], name: 'index_friendships_on_user_id_and_friend_id', unique: true - t.index ['user_id'], name: 'index_friendships_on_user_id' + create_table "friendships", force: :cascade do |t| + t.bigint "user_id" + t.bigint "friend_id" + t.boolean "accepted", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["friend_id"], name: "index_friendships_on_friend_id" + t.index ["user_id", "friend_id"], name: "index_friendships_on_user_id_and_friend_id", unique: true + t.index ["user_id"], name: "index_friendships_on_user_id" end - create_table 'group_memberships', force: :cascade do |t| - t.bigint 'group_id', null: false - t.bigint 'friend_id', null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['friend_id'], name: 'index_group_memberships_on_friend_id' - t.index ['group_id'], name: 'index_group_memberships_on_group_id' + create_table "group_memberships", force: :cascade do |t| + t.bigint "group_id", null: false + t.bigint "friend_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["friend_id"], name: "index_group_memberships_on_friend_id" + t.index ["group_id"], name: "index_group_memberships_on_group_id" end - create_table 'groups', force: :cascade do |t| - t.string 'name' - t.bigint 'user_id', null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['user_id'], name: 'index_groups_on_user_id' + create_table "groups", force: :cascade do |t| + t.string "name" + t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["user_id"], name: "index_groups_on_user_id" end - create_table 'happy_thing_group_shares', force: :cascade do |t| - t.bigint 'happy_thing_id', null: false - t.bigint 'group_id', null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['group_id'], name: 'index_happy_thing_group_shares_on_group_id' - t.index ['happy_thing_id'], name: 'index_happy_thing_group_shares_on_happy_thing_id' + create_table "happy_thing_group_shares", force: :cascade do |t| + t.bigint "happy_thing_id", null: false + t.bigint "group_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["group_id"], name: "index_happy_thing_group_shares_on_group_id" + t.index ["happy_thing_id"], name: "index_happy_thing_group_shares_on_happy_thing_id" end - create_table 'happy_thing_user_shares', force: :cascade do |t| - t.bigint 'happy_thing_id', null: false - t.bigint 'friend_id', null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['friend_id'], name: 'index_happy_thing_user_shares_on_friend_id' - t.index ['happy_thing_id'], name: 'index_happy_thing_user_shares_on_happy_thing_id' + create_table "happy_thing_user_shares", force: :cascade do |t| + t.bigint "happy_thing_id", null: false + t.bigint "friend_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["friend_id"], name: "index_happy_thing_user_shares_on_friend_id" + t.index ["happy_thing_id"], name: "index_happy_thing_user_shares_on_happy_thing_id" end - create_table 'happy_things', force: :cascade do |t| - t.string 'title', null: false - t.text 'body' - t.integer 'status' - t.bigint 'user_id', null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.datetime 'start_time' - t.string 'place' - t.float 'latitude' - t.float 'longitude' - t.bigint 'category_id' - t.boolean 'share_location' - t.index ['category_id'], name: 'index_happy_things_on_category_id' - t.index ['user_id'], name: 'index_happy_things_on_user_id' + create_table "happy_things", force: :cascade do |t| + t.string "title", null: false + t.text "body" + t.integer "status" + t.bigint "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.datetime "start_time" + t.string "place" + t.float "latitude" + t.float "longitude" + t.bigint "category_id" + t.boolean "share_location" + t.string "visibility", default: "public" + t.index ["category_id"], name: "index_happy_things_on_category_id" + t.index ["user_id"], name: "index_happy_things_on_user_id" + t.index ["visibility"], name: "index_happy_things_on_visibility" end - create_table 'users', force: :cascade do |t| - t.string 'first_name' - t.string 'last_name' - t.string 'email', default: '', null: false - t.string 'encrypted_password', default: '', null: false - t.string 'reset_password_token' - t.datetime 'reset_password_sent_at' - t.datetime 'remember_created_at' - t.integer 'sign_in_count', default: 0, null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.string 'emoji' - t.boolean 'email_opt_in', default: false - t.boolean 'location_opt_in', default: false - t.string 'username' - t.string 'confirmation_token' - t.datetime 'confirmed_at' - t.datetime 'confirmation_sent_at' - t.string 'unconfirmed_email' - t.string 'provider' - t.string 'uid' - t.index ['confirmation_token'], name: 'index_users_on_confirmation_token', unique: true - t.index ['email'], name: 'index_users_on_email', unique: true - t.index ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true + create_table "users", force: :cascade do |t| + t.string "first_name" + t.string "last_name" + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.integer "sign_in_count", default: 0, null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "emoji" + t.boolean "email_opt_in", default: false + t.boolean "location_opt_in", default: false + t.string "username" + t.string "confirmation_token" + t.datetime "confirmed_at" + t.datetime "confirmation_sent_at" + t.string "unconfirmed_email" + t.string "provider" + t.string "uid" + t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true + t.index ["email"], name: "index_users_on_email", unique: true + t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end - add_foreign_key 'active_storage_attachments', 'active_storage_blobs', column: 'blob_id' - add_foreign_key 'active_storage_variant_records', 'active_storage_blobs', column: 'blob_id' - add_foreign_key 'comments', 'happy_things' - add_foreign_key 'comments', 'users' - add_foreign_key 'friendships', 'users' - add_foreign_key 'friendships', 'users', column: 'friend_id' - add_foreign_key 'group_memberships', 'groups' - add_foreign_key 'group_memberships', 'users', column: 'friend_id' - add_foreign_key 'groups', 'users' - add_foreign_key 'happy_thing_group_shares', 'groups' - add_foreign_key 'happy_thing_group_shares', 'happy_things' - add_foreign_key 'happy_thing_user_shares', 'happy_things' - add_foreign_key 'happy_thing_user_shares', 'users', column: 'friend_id' - add_foreign_key 'happy_things', 'categories' - add_foreign_key 'happy_things', 'users' + add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" + add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "comments", "happy_things" + add_foreign_key "comments", "users" + add_foreign_key "daily_happy_email_deliveries", "users" + add_foreign_key "daily_happy_email_deliveries", "users", column: "recipient_id" + add_foreign_key "friendships", "users" + add_foreign_key "friendships", "users", column: "friend_id" + add_foreign_key "group_memberships", "groups" + add_foreign_key "group_memberships", "users", column: "friend_id" + add_foreign_key "groups", "users" + add_foreign_key "happy_thing_group_shares", "groups" + add_foreign_key "happy_thing_group_shares", "happy_things" + add_foreign_key "happy_thing_user_shares", "happy_things" + add_foreign_key "happy_thing_user_shares", "users", column: "friend_id" + add_foreign_key "happy_things", "categories" + add_foreign_key "happy_things", "users" end diff --git a/db/seeds.rb b/db/seeds.rb index f55ab61c..bc2316ee 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -50,18 +50,13 @@ def u(name) # --- Friendships --- Friendship.create!([ { user: u('Emmsiboom'), friend: u('Joshy'), accepted: true }, - { user: u('Joshy'), friend: u('Emmsiboom'), accepted: true }, { user: u('Emmsiboom'), friend: u('Hansibaby'), accepted: true }, - { user: u('Hansibaby'), friend: u('Emmsiboom'), accepted: true }, { user: u('Emmsiboom'), friend: u('Juanfairy'), accepted: true }, - { user: u('Juanfairy'), friend: u('Emmsiboom'), accepted: true }, { user: u('Mäx'), friend: u('Emmsiboom'), accepted: false }, { user: u('Santimaus'), friend: u('Emmsiboom'), accepted: false }, { user: u('Emmsiboom'), friend: u('Florenke'), accepted: false }, { user: u('Joshy'), friend: u('Hansibaby'), accepted: true }, - { user: u('Hansibaby'), friend: u('Joshy'), accepted: true }, { user: u('Juanfairy'), friend: u('Santimaus'), accepted: true }, - { user: u('Santimaus'), friend: u('Juanfairy'), accepted: true }, { user: u('Mäx'), friend: u('Joshy'), accepted: false }, { user: u('Juanfairy'), friend: u('Mäx'), accepted: false } ]) diff --git a/lib/tasks/annotate_rb.rake b/lib/tasks/annotate_rb.rake new file mode 100644 index 00000000..1ad0ec39 --- /dev/null +++ b/lib/tasks/annotate_rb.rake @@ -0,0 +1,8 @@ +# This rake task was added by annotate_rb gem. + +# Can set `ANNOTATERB_SKIP_ON_DB_TASKS` to be anything to skip this +if Rails.env.development? && ENV["ANNOTATERB_SKIP_ON_DB_TASKS"].nil? + require "annotate_rb" + + AnnotateRb::Core.load_rake_tasks +end diff --git a/lib/tasks/pwa_icons.rake b/lib/tasks/pwa_icons.rake new file mode 100644 index 00000000..b9006c61 --- /dev/null +++ b/lib/tasks/pwa_icons.rake @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +# rubocop:disable Metrics/BlockLength +namespace :assets do + desc 'Generate PWA icons in multiple sizes from source image' + task copy_icons: :environment do + require 'fileutils' + require 'mini_magick' + + source = Rails.root.join('app/assets/images/five.png') + + # Warn if source file doesn't exist + unless File.exist?(source) + puts "❌ Source image not found: #{source}" + puts 'Please ensure app/assets/images/five.png exists and is at least 512x512px' + exit 1 + end + + # Check source image dimensions + image = MiniMagick::Image.open(source) + if image.width < 512 || image.height < 512 + puts "⚠️ Warning: Source image is #{image.width}x#{image.height}. Recommended minimum is 512x512px" + end + + # Define icon sizes: [destination_path, size] + icons = [ + [Rails.root.join('public/apple-touch-icon.png'), 180], + [Rails.root.join('public/apple-touch-icon-precomposed.png'), 180], + [Rails.root.join('public/icon-192.png'), 192], + [Rails.root.join('public/icon-512.png'), 512] + ] + + icons.each do |dest, size| + resized_image = MiniMagick::Image.open(source) + resized_image.resize "#{size}x#{size}" + resized_image.write(dest) + puts "✅ Generated #{dest.basename} (#{size}x#{size})" + end + + puts "\n🎉 PWA icons updated successfully!" + end +end +# rubocop:enable Metrics/BlockLength diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 00000000..cdf83fe7 --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,30 @@ +{ + "name": "5 Things", + "short_name": "5 Things", + "description": "Track your daily happy moments", + "start_url": "/", + "display": "standalone", + "theme_color": "#FAF9F6", + "background_color": "#FAF9F6", + "orientation": "portrait", + "icons": [ + { + "src": "/apple-touch-icon.png", + "sizes": "180x180", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/spec/factories/categories.rb b/spec/factories/categories.rb index 29be2cda..a52c3f6a 100644 --- a/spec/factories/categories.rb +++ b/spec/factories/categories.rb @@ -1,5 +1,14 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: categories +# +# id :bigint not null, primary key +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# FactoryBot.define do factory :category do name { 'General' } diff --git a/spec/factories/comments.rb b/spec/factories/comments.rb index 1c8ef609..4b7d3ee5 100644 --- a/spec/factories/comments.rb +++ b/spec/factories/comments.rb @@ -1,5 +1,26 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: comments +# +# id :bigint not null, primary key +# content :text +# created_at :datetime not null +# updated_at :datetime not null +# happy_thing_id :bigint not null +# user_id :bigint not null +# +# Indexes +# +# index_comments_on_happy_thing_id (happy_thing_id) +# index_comments_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (happy_thing_id => happy_things.id) +# fk_rails_... (user_id => users.id) +# FactoryBot.define do factory :comment do content { Faker::Lorem.sentence } diff --git a/spec/factories/friendships.rb b/spec/factories/friendships.rb index 2298ddee..a5454e2c 100644 --- a/spec/factories/friendships.rb +++ b/spec/factories/friendships.rb @@ -1,5 +1,27 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: friendships +# +# id :bigint not null, primary key +# accepted :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# friend_id :bigint +# user_id :bigint +# +# Indexes +# +# index_friendships_on_friend_id (friend_id) +# index_friendships_on_user_id (user_id) +# index_friendships_on_user_id_and_friend_id (user_id,friend_id) UNIQUE +# +# Foreign Keys +# +# fk_rails_... (friend_id => users.id) +# fk_rails_... (user_id => users.id) +# FactoryBot.define do factory :friendship do user diff --git a/spec/factories/group_memberships.rb b/spec/factories/group_memberships.rb index be771bb1..fcb1bd1e 100644 --- a/spec/factories/group_memberships.rb +++ b/spec/factories/group_memberships.rb @@ -1,5 +1,25 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: group_memberships +# +# id :bigint not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# friend_id :bigint not null +# group_id :bigint not null +# +# Indexes +# +# index_group_memberships_on_friend_id (friend_id) +# index_group_memberships_on_group_id (group_id) +# +# Foreign Keys +# +# fk_rails_... (friend_id => users.id) +# fk_rails_... (group_id => groups.id) +# FactoryBot.define do factory :group_membership do association :group diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb index 7a4e2afa..f86231f5 100644 --- a/spec/factories/groups.rb +++ b/spec/factories/groups.rb @@ -1,5 +1,23 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: groups +# +# id :bigint not null, primary key +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# +# Indexes +# +# index_groups_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# FactoryBot.define do factory :group do name { 'Favorites' } diff --git a/spec/factories/happy_thing_group_shares.rb b/spec/factories/happy_thing_group_shares.rb index 7c4b1b1b..5915ff04 100644 --- a/spec/factories/happy_thing_group_shares.rb +++ b/spec/factories/happy_thing_group_shares.rb @@ -1,5 +1,25 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: happy_thing_group_shares +# +# id :bigint not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# group_id :bigint not null +# happy_thing_id :bigint not null +# +# Indexes +# +# index_happy_thing_group_shares_on_group_id (group_id) +# index_happy_thing_group_shares_on_happy_thing_id (happy_thing_id) +# +# Foreign Keys +# +# fk_rails_... (group_id => groups.id) +# fk_rails_... (happy_thing_id => happy_things.id) +# FactoryBot.define do factory :happy_thing_group_share do association :happy_thing diff --git a/spec/factories/happy_thing_user_shares.rb b/spec/factories/happy_thing_user_shares.rb index 96132e8d..cfc14cf5 100644 --- a/spec/factories/happy_thing_user_shares.rb +++ b/spec/factories/happy_thing_user_shares.rb @@ -1,5 +1,25 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: happy_thing_user_shares +# +# id :bigint not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# friend_id :bigint not null +# happy_thing_id :bigint not null +# +# Indexes +# +# index_happy_thing_user_shares_on_friend_id (friend_id) +# index_happy_thing_user_shares_on_happy_thing_id (happy_thing_id) +# +# Foreign Keys +# +# fk_rails_... (friend_id => users.id) +# fk_rails_... (happy_thing_id => happy_things.id) +# FactoryBot.define do factory :happy_thing_user_share do happy_thing diff --git a/spec/factories/happy_things.rb b/spec/factories/happy_things.rb index 205648c0..5a30eb64 100644 --- a/spec/factories/happy_things.rb +++ b/spec/factories/happy_things.rb @@ -1,5 +1,35 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: happy_things +# +# id :bigint not null, primary key +# body :text +# latitude :float +# longitude :float +# place :string +# share_location :boolean +# start_time :datetime +# status :integer +# title :string not null +# visibility :string default("public") +# created_at :datetime not null +# updated_at :datetime not null +# category_id :bigint +# user_id :bigint not null +# +# Indexes +# +# index_happy_things_on_category_id (category_id) +# index_happy_things_on_user_id (user_id) +# index_happy_things_on_visibility (visibility) +# +# Foreign Keys +# +# fk_rails_... (category_id => categories.id) +# fk_rails_... (user_id => users.id) +# FactoryBot.define do factory :happy_thing do title { 'Today I was happy' } diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 534b4486..b7c2aab2 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -1,5 +1,37 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: users +# +# id :bigint not null, primary key +# confirmation_sent_at :datetime +# confirmation_token :string +# confirmed_at :datetime +# email :string default(""), not null +# email_opt_in :boolean default(FALSE) +# emoji :string +# encrypted_password :string default(""), not null +# first_name :string +# last_name :string +# location_opt_in :boolean default(FALSE) +# provider :string +# remember_created_at :datetime +# reset_password_sent_at :datetime +# reset_password_token :string +# sign_in_count :integer default(0), not null +# uid :string +# unconfirmed_email :string +# username :string +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_users_on_confirmation_token (confirmation_token) UNIQUE +# index_users_on_email (email) UNIQUE +# index_users_on_reset_password_token (reset_password_token) UNIQUE +# FactoryBot.define do factory :user do first_name { %w[Emmsi Leamaus Juanfairy].sample } diff --git a/spec/models/friendship_spec.rb b/spec/models/friendship_spec.rb index 8579581b..753cccbb 100644 --- a/spec/models/friendship_spec.rb +++ b/spec/models/friendship_spec.rb @@ -1,5 +1,27 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: friendships +# +# id :bigint not null, primary key +# accepted :boolean default(FALSE) +# created_at :datetime not null +# updated_at :datetime not null +# friend_id :bigint +# user_id :bigint +# +# Indexes +# +# index_friendships_on_friend_id (friend_id) +# index_friendships_on_user_id (user_id) +# index_friendships_on_user_id_and_friend_id (user_id,friend_id) UNIQUE +# +# Foreign Keys +# +# fk_rails_... (friend_id => users.id) +# fk_rails_... (user_id => users.id) +# require 'rails_helper' RSpec.describe Friendship, type: :model do @@ -22,18 +44,28 @@ describe 'scopes' do let!(:user1) { create(:user) } let!(:user2) { create(:user) } + let!(:user3) { create(:user) } + # With bidirectional friendships, creating one friendship creates TWO records let!(:accepted_friendship) { create(:friendship, user: user1, friend: user2, accepted: true) } - let!(:pending_friendship) { create(:friendship, user: user2, friend: user1, accepted: false) } + let!(:pending_friendship) { create(:friendship, user: user1, friend: user3, accepted: false) } it 'returns accepted friendships' do - expect(Friendship.accepted).to include(accepted_friendship) - expect(Friendship.accepted).not_to include(pending_friendship) + accepted_friendships = Friendship.accepted + # Should include both the original AND the inverse + expect(accepted_friendships.pluck(:user_id, :friend_id)).to include( + [user1.id, user2.id], + [user2.id, user1.id] + ) end it 'returns pending friendships' do - expect(Friendship.pending).to include(pending_friendship) - expect(Friendship.pending).not_to include(accepted_friendship) + pending_friendships = Friendship.pending + # Should include both the original AND the inverse + expect(pending_friendships.pluck(:user_id, :friend_id)).to include( + [user1.id, user3.id], + [user3.id, user1.id] + ) end end end diff --git a/spec/models/group_membership_spec.rb b/spec/models/group_membership_spec.rb index ac4284ca..1ed7e276 100644 --- a/spec/models/group_membership_spec.rb +++ b/spec/models/group_membership_spec.rb @@ -1,5 +1,25 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: group_memberships +# +# id :bigint not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# friend_id :bigint not null +# group_id :bigint not null +# +# Indexes +# +# index_group_memberships_on_friend_id (friend_id) +# index_group_memberships_on_group_id (group_id) +# +# Foreign Keys +# +# fk_rails_... (friend_id => users.id) +# fk_rails_... (group_id => groups.id) +# require 'rails_helper' RSpec.describe GroupMembership, type: :model do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index a051aa68..f1895228 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1,5 +1,23 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: groups +# +# id :bigint not null, primary key +# name :string +# created_at :datetime not null +# updated_at :datetime not null +# user_id :bigint not null +# +# Indexes +# +# index_groups_on_user_id (user_id) +# +# Foreign Keys +# +# fk_rails_... (user_id => users.id) +# require 'rails_helper' RSpec.describe Group, type: :model do diff --git a/spec/models/happy_thing_group_share_spec.rb b/spec/models/happy_thing_group_share_spec.rb index 3d29769e..31910b78 100644 --- a/spec/models/happy_thing_group_share_spec.rb +++ b/spec/models/happy_thing_group_share_spec.rb @@ -1,5 +1,25 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: happy_thing_group_shares +# +# id :bigint not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# group_id :bigint not null +# happy_thing_id :bigint not null +# +# Indexes +# +# index_happy_thing_group_shares_on_group_id (group_id) +# index_happy_thing_group_shares_on_happy_thing_id (happy_thing_id) +# +# Foreign Keys +# +# fk_rails_... (group_id => groups.id) +# fk_rails_... (happy_thing_id => happy_things.id) +# require 'rails_helper' RSpec.describe HappyThingGroupShare, type: :model do diff --git a/spec/models/happy_thing_spec.rb b/spec/models/happy_thing_spec.rb index 26ef921c..1d1e82b9 100644 --- a/spec/models/happy_thing_spec.rb +++ b/spec/models/happy_thing_spec.rb @@ -1,5 +1,35 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: happy_things +# +# id :bigint not null, primary key +# body :text +# latitude :float +# longitude :float +# place :string +# share_location :boolean +# start_time :datetime +# status :integer +# title :string not null +# visibility :string default("public") +# created_at :datetime not null +# updated_at :datetime not null +# category_id :bigint +# user_id :bigint not null +# +# Indexes +# +# index_happy_things_on_category_id (category_id) +# index_happy_things_on_user_id (user_id) +# index_happy_things_on_visibility (visibility) +# +# Foreign Keys +# +# fk_rails_... (category_id => categories.id) +# fk_rails_... (user_id => users.id) +# require 'rails_helper' RSpec.describe HappyThing, type: :model do @@ -43,9 +73,9 @@ user = create(:user) friends = create_list(:user, 3) + # With bidirectional friendships, creating one friendship creates both records friends.each do |friend| - create(:friendship, user:, friend:) - create(:friendship, user: friend, friend: user) + create(:friendship, user:, friend:, accepted: true) end perform_enqueued_jobs do @@ -69,9 +99,9 @@ friends = create_list(:user, 3) non_friend = create(:user) + # With bidirectional friendships, creating one friendship creates both records friends.each do |friend| - create(:friendship, user:, friend:) - create(:friendship, user: friend, friend: user) + create(:friendship, user:, friend:, accepted: true) end create_list(:happy_thing, 4, user:) diff --git a/spec/models/happy_thing_user_share_spec.rb b/spec/models/happy_thing_user_share_spec.rb index 34a7b1d0..7958130a 100644 --- a/spec/models/happy_thing_user_share_spec.rb +++ b/spec/models/happy_thing_user_share_spec.rb @@ -1,5 +1,25 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: happy_thing_user_shares +# +# id :bigint not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# friend_id :bigint not null +# happy_thing_id :bigint not null +# +# Indexes +# +# index_happy_thing_user_shares_on_friend_id (friend_id) +# index_happy_thing_user_shares_on_happy_thing_id (happy_thing_id) +# +# Foreign Keys +# +# fk_rails_... (friend_id => users.id) +# fk_rails_... (happy_thing_id => happy_things.id) +# require 'rails_helper' RSpec.describe HappyThingUserShare, type: :model do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ad3b85d1..46db60c1 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,5 +1,37 @@ # frozen_string_literal: true +# == Schema Information +# +# Table name: users +# +# id :bigint not null, primary key +# confirmation_sent_at :datetime +# confirmation_token :string +# confirmed_at :datetime +# email :string default(""), not null +# email_opt_in :boolean default(FALSE) +# emoji :string +# encrypted_password :string default(""), not null +# first_name :string +# last_name :string +# location_opt_in :boolean default(FALSE) +# provider :string +# remember_created_at :datetime +# reset_password_sent_at :datetime +# reset_password_token :string +# sign_in_count :integer default(0), not null +# uid :string +# unconfirmed_email :string +# username :string +# created_at :datetime not null +# updated_at :datetime not null +# +# Indexes +# +# index_users_on_confirmation_token (confirmation_token) UNIQUE +# index_users_on_email (email) UNIQUE +# index_users_on_reset_password_token (reset_password_token) UNIQUE +# require 'rails_helper' RSpec.describe User, type: :model do @@ -133,9 +165,8 @@ context 'when unconfirmed manual user tries OAuth' do it 'links OAuth and auto-confirms existing account' do - existing_user = create(:user, - email: auth_hash.info.email, - confirmed_at: nil) + existing_user = create(:user, email: auth_hash.info.email) + existing_user.update_column(:confirmed_at, nil) user = User.from_omniauth(auth_hash) existing_user.reload diff --git a/spec/requests/friendships_spec.rb b/spec/requests/friendships_spec.rb index dfa15bd8..510a4c3f 100644 --- a/spec/requests/friendships_spec.rb +++ b/spec/requests/friendships_spec.rb @@ -7,14 +7,14 @@ let(:friend) { create(:user) } before do - sign_in user + sign_in user, scope: :user end describe 'POST /create' do it 'creates a new friendship' do expect do post friendships_path, params: { friend_id: friend.id } - end.to change(Friendship, :count).by(1) + end.to change(Friendship, :count).by(2) # Bidirectional: creates 2 records expect(response).to have_http_status(:redirect) # or :success end end @@ -26,6 +26,9 @@ put friendship_path(friendship), params: { friendship: { accepted: true } } expect(response).to have_http_status(:redirect) expect(friendship.reload.accepted).to eq(true) + # Check that inverse was also updated + inverse = Friendship.find_by(user_id: friend.id, friend_id: user.id) + expect(inverse.accepted).to eq(true) end end @@ -35,7 +38,7 @@ it 'deletes the friendship' do expect do delete friendship_path(friendship) - end.to change(Friendship, :count).by(-1) + end.to change(Friendship, :count).by(-2) # Bidirectional: deletes 2 records expect(response).to have_http_status(:redirect) end end diff --git a/spec/requests/happy_things_spec.rb b/spec/requests/happy_things_spec.rb index 6dafd696..a2ff791b 100644 --- a/spec/requests/happy_things_spec.rb +++ b/spec/requests/happy_things_spec.rb @@ -8,11 +8,9 @@ let(:groupie) { create(:user, first_name: 'Groupie') } let(:stranger) { create(:user, first_name: 'Stranger') } + # With bidirectional friendships, creating one friendship creates both records let!(:friendship_one) { create(:friendship, user: owner, friend:, accepted: true) } - let!(:friendship_two) { create(:friendship, user: friend, friend: owner, accepted: true) } - let!(:friendship_three) { create(:friendship, user: owner, friend: groupie, accepted: true) } - let!(:friendship_four) { create(:friendship, user: groupie, friend: owner, accepted: true) } let!(:happy_thing_user_shared) { create(:happy_thing, user: owner, title: 'Directly Shared') } let!(:happy_thing_group_shared) { create(:happy_thing, user: owner, title: 'Group Shared') } @@ -26,19 +24,19 @@ describe 'visibility rules' do it 'shows directly shared happy thing to the friend' do - sign_in friend + sign_in friend, scope: :user get root_path expect(response.body).to include('Directly Shared') end it 'shows group shared happy thing to the group member' do - sign_in groupie + sign_in groupie, scope: :user get root_path expect(response.body).to include('Group Shared') end it 'shows happy thing to the owner' do - sign_in owner + sign_in owner, scope: :user get root_path expect(response.body).to include('Directly Shared') expect(response.body).to include('Group Shared') @@ -46,7 +44,7 @@ end it 'does not show private happy thing to a stranger' do - sign_in stranger + sign_in stranger, scope: :user get root_path expect(response.body).not_to include('Directly Shared') expect(response.body).not_to include('Group Shared') @@ -56,29 +54,31 @@ describe 'location sharing' do it 'saves location when share_location is checked' do - sign_in owner + sign_in owner, scope: :user expect do post happy_things_path, params: { happy_thing: { title: 'Shared with location', share_location: '1', - place: 'Berlin' + place: 'Berlin', + latitude: 52.5173885, + longitude: 13.3951309 } } end.to change(HappyThing, :count).by(1) happy_thing = HappyThing.last expect(happy_thing.share_location).to be(true) - expect(happy_thing.latitude).to be_within(0.001).of(52.5173885) - expect(happy_thing.longitude).to be_within(0.001).of(13.3951309) expect(happy_thing.place).to eq('Berlin') + expect(happy_thing.latitude).to eq(52.5173885) + expect(happy_thing.longitude).to eq(13.3951309) end end describe 'GET /calendar' do it 'returns a success response' do - sign_in owner + sign_in owner, scope: :user get calendar_path expect(response).to have_http_status(:success) expect(response.body).to include('Tue') @@ -87,7 +87,7 @@ describe 'GET /friends/happy_things' do it 'returns a success response' do - sign_in owner + sign_in owner, scope: :user get friends_happy_things_path expect(response).to have_http_status(:success) expect(response.body).to include('What made you happy') @@ -96,7 +96,7 @@ describe 'GET /through_the_years' do it 'returns a success response' do - sign_in owner + sign_in owner, scope: :user get through_the_years_path expect(response).to have_http_status(:success) expect(response.body).to include('Add a Happy Thing from a past year') @@ -105,7 +105,7 @@ describe 'GET /future_root' do it 'returns a success response' do - sign_in owner + sign_in owner, scope: :user get future_root_path expect(response).to have_http_status(:success) expect(response.body).to include('What made you smile today?') diff --git a/spec/requests/users_spec.rb b/spec/requests/users_spec.rb index 9724a233..15f0ec2c 100644 --- a/spec/requests/users_spec.rb +++ b/spec/requests/users_spec.rb @@ -10,13 +10,6 @@ end describe 'CRUD operations' do - describe 'GET /index' do - it 'returns http success' do - get '/users' - expect(response).to have_http_status(:success) - end - end - describe 'GET /show' do it 'returns http success' do user = create(:user) @@ -101,7 +94,7 @@ it 'returns http success' do get friends_path expect(response).to have_http_status(:success) - expect(response.body).to include('My Friends') + expect(response.body).to include(I18n.t('friends.page_title')) end end @@ -109,7 +102,8 @@ it 'returns http success' do get profile_path expect(response).to have_http_status(:success) - expect(response.body).to include('My Profile') + expect(response.body).to include('Happy Count') + expect(response.body).to include(@user.first_name) end end end diff --git a/spec/system/happy_things_crud_spec.rb b/spec/system/happy_things_crud_spec.rb index 840bb5ce..3c311e4d 100644 --- a/spec/system/happy_things_crud_spec.rb +++ b/spec/system/happy_things_crud_spec.rb @@ -38,12 +38,11 @@ fill_in 'Name', with: 'cute mirror wtf' select 'Spiritual & Mind', from: 'happy_thing_category_id' attach_file 'happy_thing[photo]', Rails.root.join('spec/fixtures/test_image.jpg') - check 'Share my location' expect do - click_on 'Create happy thing' + find('input[type="submit"]').click - expect(page).to have_content('Happy Thing was successfully created.') + expect(page).to have_content('Happy Thing was successfully created.', wait: 10) end.to change(HappyThing, :count).by(1) created_happy_thing = HappyThing.last @@ -61,9 +60,9 @@ expect(page).to have_current_path(edit_happy_thing_path(happy_thing)) fill_in 'Name', with: 'fresh new title' - click_button('Update happy thing') + find('input[type="submit"]').click - expect(page).to have_content('Yay! 🎉 Happy Thing was updated 🥰') + expect(page).to have_content('Yay! 🎉 Happy Thing was updated 🥰', wait: 10) expect(page).to have_content('fresh new title') expect(page).not_to have_content(happy_thing.title, wait: 5) end