diff --git a/Gemfile b/Gemfile index 1a1aade..80a6d8e 100644 --- a/Gemfile +++ b/Gemfile @@ -41,7 +41,7 @@ gem "tzinfo-data", platforms: [:windows, :jruby] gem "bootsnap", require: false # Use Active Storage variants [https://guides.rubyonrails.org/active_storage_overview.html#transforming-images] -# gem "image_processing", "~> 1.2" +gem "image_processing", "~> 1.2" group :development, :test do # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem diff --git a/Gemfile.lock b/Gemfile.lock index 732ee28..e600305 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -124,12 +124,21 @@ GEM railties (>= 5.0.0) faker (3.4.2) i18n (>= 1.8.11, < 2) + ffi (1.17.0-aarch64-linux-gnu) + ffi (1.17.0-arm-linux-gnu) + ffi (1.17.0-arm64-darwin) + ffi (1.17.0-x86-linux-gnu) + ffi (1.17.0-x86_64-darwin) + ffi (1.17.0-x86_64-linux-gnu) friendly_id (5.5.1) activerecord (>= 4.0.0) globalid (1.2.1) activesupport (>= 6.1) i18n (1.14.5) concurrent-ruby (~> 1.0) + image_processing (1.13.0) + mini_magick (>= 4.9.5, < 5) + ruby-vips (>= 2.0.17, < 3) inertia_rails (3.2.0) railties (>= 5) io-console (0.7.2) @@ -155,6 +164,7 @@ GEM net-smtp marcel (1.0.4) matrix (0.4.2) + mini_magick (4.13.2) mini_mime (1.1.5) minitest (5.25.1) msgpack (1.7.2) @@ -295,6 +305,9 @@ GEM ruby-lsp-rspec (0.1.12) ruby-lsp (~> 0.17.0) ruby-progressbar (1.13.0) + ruby-vips (2.2.2) + ffi (~> 1.12) + logger rubyzip (2.3.2) securerandom (0.3.1) selenium-webdriver (4.24.0) @@ -380,6 +393,7 @@ DEPENDENCIES factory_bot_rails (~> 6.4) faker (~> 3.3) friendly_id (~> 5.5) + image_processing (~> 1.2) inertia_rails (~> 3.1) jbuilder oj_serializers (~> 2.0) diff --git a/app/controllers/companies_controller.rb b/app/controllers/companies_controller.rb index 411a150..48ef4ca 100644 --- a/app/controllers/companies_controller.rb +++ b/app/controllers/companies_controller.rb @@ -18,6 +18,6 @@ def show private def company_scope - Company.includes(:technologies, :continent, :country, :region, :city) + Company.includes(:technologies, :continent, :country, :region, :city, :logo_attachment) end end diff --git a/app/frontend/pages/companies/show.tsx b/app/frontend/pages/companies/show.tsx index 78c8d3f..42f6e65 100644 --- a/app/frontend/pages/companies/show.tsx +++ b/app/frontend/pages/companies/show.tsx @@ -25,6 +25,7 @@ function Show({ company }: Props) { return (
+

{company.name}

{company.website && ( diff --git a/app/frontend/types/company.ts b/app/frontend/types/company.ts index 15f2847..efdcbc8 100644 --- a/app/frontend/types/company.ts +++ b/app/frontend/types/company.ts @@ -14,6 +14,7 @@ export interface Company { medium: string; full: string; }; + logoUrl?: string; continent: GeoFragment; country: GeoFragment; region: GeoFragment; diff --git a/app/models/company.rb b/app/models/company.rb index 7652c29..916c76c 100644 --- a/app/models/company.rb +++ b/app/models/company.rb @@ -14,6 +14,8 @@ class Company < ApplicationRecord has_one :country, through: :region has_one :continent, through: :country + has_one_attached :logo + validates :name, presence: true validates :website, url: { no_local: true, public_suffix: true } validates :careers_page, url: { no_local: true, public_suffix: true }, allow_blank: true diff --git a/app/serializers/company_serializer.rb b/app/serializers/company_serializer.rb index d23dd41..7d244a0 100644 --- a/app/serializers/company_serializer.rb +++ b/app/serializers/company_serializer.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class CompanySerializer < ApplicationSerializer + include Rails.application.routes.url_helpers + attributes( :id, :slug, @@ -18,6 +20,12 @@ class CompanySerializer < ApplicationSerializer } end + attribute :logo_url do + return unless company&.logo&.attached? + + rails_representation_url(company.logo.variant(resize_to_fit: [100, 100]).processed, only_path: true) + end + has_many :technologies, serializer: TechnologySerializer has_one :continent, serializer: GeoFragmentSerializer has_one :country, serializer: GeoFragmentSerializer diff --git a/db/migrate/20240908134818_create_active_storage_tables.active_storage.rb b/db/migrate/20240908134818_create_active_storage_tables.active_storage.rb new file mode 100644 index 0000000..9be0353 --- /dev/null +++ b/db/migrate/20240908134818_create_active_storage_tables.active_storage.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[7.0] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table(:active_storage_blobs, id: primary_key_type) 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) + + if connection.supports_datetime_with_precision? + t.datetime(:created_at, precision: 6, null: false) + else + t.datetime(:created_at, null: false) + end + + t.index([:key], unique: true) + end + + create_table(:active_storage_attachments, id: primary_key_type) do |t| + t.string(:name, null: false) + t.references(:record, null: false, polymorphic: true, index: false, type: foreign_key_type) + t.references(:blob, null: false, type: foreign_key_type) + + if connection.supports_datetime_with_precision? + t.datetime(:created_at, precision: 6, null: false) + else + t.datetime(:created_at, null: false) + end + + t.index( + [:record_type, :record_id, :name, :blob_id], + name: :index_active_storage_attachments_uniqueness, + unique: true, + ) + t.foreign_key(:active_storage_blobs, column: :blob_id) + end + + create_table(:active_storage_variant_records, id: primary_key_type) do |t| + t.belongs_to(:blob, null: false, index: false, type: foreign_key_type) + t.string(:variation_digest, null: false) + + t.index([:blob_id, :variation_digest], name: :index_active_storage_variant_records_uniqueness, unique: true) + t.foreign_key(:active_storage_blobs, column: :blob_id) + end + end + + private + + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/db/schema.rb b/db/schema.rb index 7c381cc..f7f664e 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -14,6 +14,34 @@ # These are extensions that must be enabled in order to support this database enable_extension "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_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_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 "cities", force: :cascade do |t| t.string "name", null: false t.bigint "region_id", null: false @@ -93,6 +121,8 @@ t.index ["name"], name: "index_technologies_on_name", 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 "cities", "regions" add_foreign_key "companies", "cities" add_foreign_key "company_technologies", "companies" diff --git a/db/seeds.rb b/db/seeds.rb index 2aa06fc..16b1128 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -85,6 +85,11 @@ def seed_companies and Internal Tool Builder for Ruby on Rails that saves engineers and teams months of development time.", city: @bucharest, technologies: [@ruby, @rails], + logo: { + io: File.open(Rails.root.join("db/seeds/avohq.png")), + filename: "avohq.png", + content_type: "image/png", + }, ) create_records(Company, 48) do |i| diff --git a/db/seeds/avohq.png b/db/seeds/avohq.png new file mode 100644 index 0000000..db073c2 Binary files /dev/null and b/db/seeds/avohq.png differ diff --git a/spec/controllers/companies_controller_spec.rb b/spec/controllers/companies_controller_spec.rb index abb82eb..b624be0 100644 --- a/spec/controllers/companies_controller_spec.rb +++ b/spec/controllers/companies_controller_spec.rb @@ -6,6 +6,8 @@ require_factories require_inertia + let(:company) { create(:company) } + describe "#index", inertia: true do it "renders a list of companies" do companies = create_list(:company, 5) @@ -19,8 +21,6 @@ describe "#show", inertia: true do it "renders a company" do - company = create(:company) - get "/companies/#{company.slug}" expect_inertia.to(render_component("companies/show")) diff --git a/spec/serializers/company_serializer_spec.rb b/spec/serializers/company_serializer_spec.rb new file mode 100644 index 0000000..e1b9fa4 --- /dev/null +++ b/spec/serializers/company_serializer_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require "rails_helper" + +RSpec.describe(CompanySerializer, type: :serializer) do + require_factories + + let(:company) { create(:company) } + subject { CompanySerializer.one(company) } + + describe "#logo_url" do + context "when logo is attached" do + let(:logo_url) { "/path/to/logo.png" } + + before do + allow(company).to(receive_message_chain(:logo, :attached?).and_return(true)) + allow(company).to(receive_message_chain(:logo, :variant, :processed).and_return( + instance_double(ActiveStorage::VariantWithRecord), + )) + allow_any_instance_of(CompanySerializer).to(receive(:rails_representation_url).and_return(logo_url)) + end + + it "returns the correct logo URL" do + expect(subject[:logoUrl]).to(eq(logo_url)) + end + end + + context "when logo is not attached" do + before do + allow(company).to(receive_message_chain(:logo, :attached?).and_return(false)) + end + + it "returns nil" do + expect(subject[:logoUrl]).to(be_nil) + end + end + end +end