Skip to content

Commit

Permalink
Add QR Code generation and sharing
Browse files Browse the repository at this point in the history
  • Loading branch information
andresag4 committed Jul 15, 2024
1 parent 07b5875 commit bedefc8
Show file tree
Hide file tree
Showing 20 changed files with 232 additions and 5 deletions.
3 changes: 3 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ gem "action_policy", "~> 0.7.0"
# Authentication
gem "bcrypt", "~> 3.1.20"

# QR code
gem "rqrcode", "~> 2.0"

# Other
gem "bootsnap", require: false
gem "puma", ">= 5.0"
Expand Down
6 changes: 6 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ GEM
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
childprocess (5.0.0)
chunky_png (1.4.0)
coderay (1.1.3)
coercible (1.0.0)
descendants_tracker (~> 0.0.1)
Expand Down Expand Up @@ -350,6 +351,10 @@ GEM
rexml (3.3.1)
strscan
rouge (4.3.0)
rqrcode (2.2.0)
chunky_png (~> 1.0)
rqrcode_core (~> 1.0)
rqrcode_core (1.2.0)
rspec (3.13.0)
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
Expand Down Expand Up @@ -522,6 +527,7 @@ DEPENDENCIES
rack-mini-profiler
rails (~> 7.1.3, >= 7.1.3.4)
rails-controller-testing
rqrcode (~> 2.0)
rspec-instafail
rspec-rails
rspec-retry
Expand Down
8 changes: 8 additions & 0 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
class ApplicationController < ActionController::Base
include Authentication

rescue_from ActiveRecord::RecordNotFound, with: :record_not_found

private

def record_not_found
render file: Rails.public_path.join("404.html").to_s, status: :not_found, layout: false
end
end
21 changes: 21 additions & 0 deletions app/controllers/profiles_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class ProfilesController < ApplicationController
before_action :set_profile, only: [:show, :qr_code]

def show
end

def qr_code
send_data(
QRCodeGenerator.new(profile_url(@profile.uuid)).as_png(size: 240),
filename: "#{@profile.name}.png",
type: "image/png",
disposition: "attachment"
)
end

private

def set_profile
@profile = Profile.public_profiles.find_by!(uuid: params[:uuid])
end
end
5 changes: 5 additions & 0 deletions app/helpers/profile_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module ProfileHelper
def qr_code_svg(profile)
QRCodeGenerator.new(profile_url(profile.uuid)).as_svg(module_size: 4)
end
end
25 changes: 25 additions & 0 deletions app/javascript/controllers/qr_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Controller } from '@hotwired/stimulus'

export default class extends Controller {
static targets = ['button']

async share () {
const button = this.buttonTarget

if (navigator.share) {
try {
const file = await fetch(button.dataset.url)
.then((response) => response.blob())
.then((blob) => new File([blob], 'qr.png', { type: 'image/png' }))

await navigator.share({
files: [file], title: 'QR Code'
})
} catch (err) {
window.open(button.dataset.url)
}
} else {
window.open(button.dataset.url)
}
}
}
21 changes: 21 additions & 0 deletions app/lib/qr_code_generator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
class QRCodeGenerator
def initialize(url)
@url = url
end

def as_svg(options = {})
qr.as_svg(options)
end

def as_png(options = {})
qr.as_png(options)
end

private

attr_reader :url

def qr
@_qr ||= RQRCode::QRCode.new(url)
end
end
14 changes: 14 additions & 0 deletions app/models/profile.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,32 @@
# name :string
# profileable_type :string not null
# twitter_url :string
# uuid :string
# created_at :datetime not null
# updated_at :datetime not null
# profileable_id :integer not null
#
# Indexes
#
# index_profiles_on_profileable (profileable_type,profileable_id)
# index_profiles_on_uuid (uuid) UNIQUE
#
class Profile < ApplicationRecord
belongs_to :profileable, polymorphic: true

has_one :self_ref, class_name: "Profile", foreign_key: :id, inverse_of: :self_ref, dependent: :destroy
has_one :user, through: :self_ref, source: :profileable, source_type: "User"
has_one :speaker, through: :self_ref, source: :profileable, source_type: "Speaker"

validates :uuid, uniqueness: true

before_create :set_uuid

scope :public_profiles, -> { where(is_public: true) }

private

def set_uuid
self.uuid = SecureRandom.uuid
end
end
2 changes: 1 addition & 1 deletion app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
</head>

<body>
<main class="container mx-auto mt-28 px-5 flex">
<main class="container mx-auto mt-28 px-5">
<div><%= notice %></div>
<div><%= alert %></div>
<% if user_signed_in? %>
Expand Down
23 changes: 23 additions & 0 deletions app/views/profiles/show.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<h1>Profile</h1>

<h2>My QR Code</h2>

<%= render inline: qr_code_svg(@profile) %>
<br>

<!-- Save QR -->
<%= link_to 'Save QR Code', qr_code_profile_path(@profile.uuid), method: :get %>
<br>
<br>

<!-- Share QR -->
<div data-controller="qr">
<button id="shareQR" data-action="click->qr#share" data-qr-target="button" data-url="<%= qr_code_profile_path(@profile.uuid) %>">
Share
</button>
</div>
<br>

<!-- Open Camera -->
<h3>Scan a code</h3>
<input type="file" accept="image/*" capture="capture">
3 changes: 3 additions & 0 deletions config/environments/development.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,7 @@

# Raise error when a before_action's only/except options reference missing actions
config.action_controller.raise_on_missing_callback_actions = true

# Allow ngrok hosts
config.hosts << /[a-z0-9.\-]+\.ngrok\.io/
end
6 changes: 3 additions & 3 deletions config/initializers/inflections.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@
# end

# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym "RESTful"
# end
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.acronym "QR"
end
5 changes: 5 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,9 @@
resource :session, only: [:new, :create, :destroy]
resource :password, only: [:edit, :update]
resource :password_reset, only: [:new, :create, :edit, :update]
resources :profiles, only: [:show, :edit, :update], param: :uuid do
member do
get :qr_code
end
end
end
6 changes: 6 additions & 0 deletions db/migrate/20240710213305_add_profile_uuid_to_profile.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AddProfileUuidToProfile < ActiveRecord::Migration[7.1]
def change
add_column :profiles, :uuid, :string
add_index :profiles, :uuid, unique: true
end
end
4 changes: 3 additions & 1 deletion db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

35 changes: 35 additions & 0 deletions spec/controllers/profiles_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe ProfilesController, type: :controller do
let(:profile) { create(:profile, :with_user, :public) }

before do
sign_in(profile.user)
end

describe "GET #show" do
it "returns a success response" do
get :show, params: {uuid: profile.uuid}
expect(response).to have_http_status(:success)
end

context "when the profile is not public" do
let(:profile) { create(:profile, :with_user, is_public: false) }

it "returns a not found response" do
get :show, params: {uuid: profile.uuid}
expect(response).to have_http_status(:not_found)
end
end
end

describe "GET #qr_code" do
it "returns a PNG" do
get :qr_code, params: {uuid: profile.uuid}
expect(response.headers["Content-Type"]).to eq("image/png")
expect(response.headers["Content-Disposition"]).to include("#{profile.name}.png")
end
end
end
7 changes: 7 additions & 0 deletions spec/factories/profiles.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
# name :string
# profileable_type :string not null
# twitter_url :string
# uuid :string
# created_at :datetime not null
# updated_at :datetime not null
# profileable_id :integer not null
#
# Indexes
#
# index_profiles_on_profileable (profileable_type,profileable_id)
# index_profiles_on_uuid (uuid) UNIQUE
#
FactoryBot.define do
factory :profile do
Expand All @@ -28,9 +30,14 @@
github_url { "https://github.com" }
linkedin_url { "https://linkedin.com" }
twitter_url { "https://twitter.com" }
uuid { SecureRandom.uuid }

trait :with_user do
association :profileable, factory: :user
end

trait :public do
is_public { true }
end
end
end
13 changes: 13 additions & 0 deletions spec/helpers/profile_helper_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe ProfileHelper, type: :helper do
describe "#qr_code_svg" do
let(:profile) { create(:profile, :with_user) }

it "generates a QR code SVG" do
expect(helper.qr_code_svg(profile)).to include("<svg")
end
end
end
19 changes: 19 additions & 0 deletions spec/lib/qr_code_generator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe QRCodeGenerator do
let(:qr_code_generator) { described_class.new(Faker::Internet.url) }

describe "#as_svg" do
it "returns an SVG" do
expect(qr_code_generator.as_svg).to include("<svg")
end
end

describe "#as_png" do
it "returns a PNG" do
expect(qr_code_generator.as_png.to_s).to include("PNG")
end
end
end
11 changes: 11 additions & 0 deletions spec/models/profile_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@
# name :string
# profileable_type :string not null
# twitter_url :string
# uuid :string
# created_at :datetime not null
# updated_at :datetime not null
# profileable_id :integer not null
#
# Indexes
#
# index_profiles_on_profileable (profileable_type,profileable_id)
# index_profiles_on_uuid (uuid) UNIQUE
#
require "rails_helper"

Expand All @@ -27,4 +29,13 @@
it "has a valid factory" do
expect(profile).to be_valid
end

describe "callbacks" do
let(:profile) { build(:profile, :with_user) }

it "sets a UUID before creation" do
profile.save
expect(profile.uuid).to be_present
end
end
end

0 comments on commit bedefc8

Please sign in to comment.