Membuat Fitur Pertemanan dengan Referral Link pada Rails
Prerequisite
ruby 2.6.3
rails 5.2.3
postgresql 11.5
Prakata
Catatan kali ini, saya akan menuliskan tentang fitur yang bertujuan untuk menambahkan teman dengan menggunakan “referral link” pada Ruby on Rails.
Permasalahan
Saya memiliki tabel users menampung data user dan tabel friendships, yang menampung data relasi antar user.
Sebagai ilustrasi user dengan ID 1 berelasi dengan user dengan ID 2 pada tebal yang sama, yaitu tabel users.
Pemecahan masalah
Migration
Pertama, buat dulu Active Record Migration untuk membuat users model.
$ rails generate model user full_name email password invitation_token
Selanjutnya, modifikasi pada file migration tersebut, seperti di bawah ini.
1class CreateUsers < ActiveRecord::Migration[5.2]2 def change3 create_table :users do |t|4 t.string :full_name5 t.string :email, default: "", null: false6 t.string :password, default: "", null: false7 t.string :invitation_token89 t.timestamps10 end11 end12end
Saya juga menambahkan field :invitation_token
untuk menampung data token yang akan disematkan (dipasang) sebagai kode unik pada akhiran referral link.
Selanjutnya, buat Active Record Migration untuk membuat friendships model.
Migration ini adalah migration yang menentukan relasi antar user pada tabel friendships.
$ rails generate model friendship
Selanjutnya, modifikasi file migration tersebut, seperti di bawah ini.
1class CreateFriendships < ActiveRecord::Migration[5.2]2 def change3 create_table :friendships do |t|4 t.belongs_to :user, foreign_key: true5 t.belongs_to :friend6 t.string :status, default: 'pending', null: 'false'78 t.timestamps9 end10 end11end
Perhatikan tipe data dari :user
dan :friend
yang merupakan belongs_to
.
Kedua field ini mengarah pada tabel users. Yang nantinya akan menyimpan data berupa ID (:user_id
dan :friend_id
) dengan tipe data integer. Dengan foreign_key yang akan dipegang oleh field :user_id
yang akan berfungsi sebagai user yang menginvite. Sedangkan, :friend_id
sebagai user yang diinvite.
Pada tabel friendships ini, saya juga menambahkan field :status
. Yang nantinya akan menampung 3 nilai.
1['pending', 'approved', 'rejected']
Field :status
ini mungkin nantinya dapat dimanfaatkan untuk fitur commission amount. Namun, pada catatan kali ini, saya tidak membahas hal ini.
Selanjutnya, tinggal menjalankan kedua migration tersebut.
$ rails db:migrate
Model
Saya akan menggunakan belongs_to
pada friendship model. Sedangkan pada user model saya akan menggunakan has_many
dan has_many :through
association.
Saya sempat membaca, dapat pula menggunakan has_and_belongs_to_many
(HABTM) association. Namun, belum saya coba.
Saya akan mulai dari user model.
1class User < ApplicationRecord2 has_many :friendships, dependent: :destroy3 has_many :friends, through: :friendships4end56# == Schema Information7#8# Table name: users9#10# id :bigint not null, primary key11# full_name :string12# email :string default(""), not null13# password :string default(""), not null14# invitation_token :string15# created_at :datetime not null16# updated_at :datetime not null17#
has_many :friendships
yang akan menampung nilai ID dari user yang menginvite. Pada tabel friendships, nilai ini akan disimpan pada field :user_id
.
Sedangkan, has_many :friends, through: :friendships
yang akan menampung nilai ID dari user yang diinvite. Pada tabel friendships, nilai akan disimpan pada field :friend_id
.
Selanjutnya untuk friendship model.
1class Friendship < ApplicationRecord2 belongs_to :user3 belongs_to :friend, class_name: 'User'4end56# == Schema Information7#8# Table name: friendships9#10# id :bigint not null, primary key11# status :string default("pending")12# friend_id :bigint13# user_id :bigint14# created_at :datetime not null15# updated_at :datetime not null16#17# Indexes18#19# index_friendships_on_friend_id (friend_id)20# index_friendships_on_user_id (user_id)21#22# Foreign Keys23#24# fk_rails_... (user_id => users.id)25#
Nah, sudah jelas sekali, kalau data yang ada di tabel friendships ini akan berasosiasi dengan tabel users. Namun, memiliki asosiasi antar user pada tabel user.
- Yang mengundang
:user
- Yang diundang
:friend
Karena :friend
juga merupakan association dari user model, maka saya perlu mendefinisikan class_name: 'User'
.
Selanjutnya, saya akan membuat sebuah method yang akan digunakan untuk mengenerate invitation_token.
Saya akan buat dengan menggunakan concern.
1module GenerateInvitationToken2 extend ActiveSupport::Concern34 included do5 before_create :generate_invitation_token6 end78 protected910 def generate_invitation_token11 begin12 self.invitation_token = self.full_name.downcase.strip.gsub(' ', '')13 generate_another_token(self.invitation_token)14 end while User.exists?(invitation_token: self.invitation_token)15 end1617 def generate_another_token(token)18 self.invitation_token = token + (User.where("invitation_token ilike ?", "%#{token}%").count + 1).to_s19 end20end
module GenerateInvitationToken di atas, akan membuatkan invitation_token dari :full_name
dan akan menambahkan nila 1 di belakangnya. Sehingga, apabila terdapat dua buah user dengan :full_name
yang sama, maka akan dibedakan berdasarkan angka terakhir pada invitation_token (kode unik).
Nah, tinggal diincludekan pada user model.
1class User < ApplicationRecord2 has_many :friendships, dependent: :destroy3 has_many :friends, through: :friendships45 include GenerateInvitationToken6end
Setelah merelasikan antar model dan , selanjutnya saya akan mencoba relasi antar user pada Rails Console.
Buat dua buah user baru.
1irb(main):001:0> budi = User.create(email: 'budibudiman@gmail.com', full_name: 'Budi Budiman', password: 'budiman')2irb(main):002:0> nina = User.create(email: 'ninaremina@gmail.com', full_name: 'Nina Remina', password: 'ninaremina')
Sekarang saya akan menggunakan method .friendships.create
, untuk membuat pertemanan antar Budi dan Nina.
Budi sebagai yang mengundang, Nina sebagai yang diundang.
1irb(main):003:0> budi.friendships.create(friend: nina)
1(0.2ms) BEGIN2Friendship Create (9.5ms) INSERT INTO "friendships" ("user_id", "friend_id", "created_at", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id" [["user_id", 21], ["friend_id", 22], ["created_at", "2020-02-10 23:06:36.308621"], ["updated_at", "2020-02-10 23:06:36.308621"]]3(2.2ms) COMMIT45=> #<Friendship id: 1, user_id: 1, friend_id: 2, status: "pending", created_at: "2020-02-10 23:06:36", updated_at: "2020-02-10 23:06:36">6
Nah, pada Active Record sudah berhasil.
Selanjutnya tinggal memasangnya ke dalam controller.
Controller
Karena proses penambahan pertemanan ini terjadi pada saat user yang diundang melakukan registration (*). Maka saya akan menambahkan fungsi penambahan pertemanan ini di user registration controller pada action create (Users::RegistrationsController#create
).
INFO
Contoh controller di bawah ini adalah registrations_controller yang dimiliki oleh Devise.
Karena saya membuat model user, menggunakan Devise generator.
1class Users::RegistrationsController < Devise::RegistrationsController2 # ...3 # ...45 def new6 if params.has_key?(:invitation_token)7 # Untuk digunakan pada hidden_field di halaman registrasi8 @invitation_token = params[:invitation_token]9 end10 super11 end1213 def create14 super15 unless params[:user][:invitation_token].blank?16 if resource.save17 # Untuk menambahkan teman yang diundang ke teman yang mengundang18 inviter = User.find_by_invitation_token(params[:user][:invitation_token])19 inviter.friendships.create(friend: resource)20 resource.save21 end22 end23 end2425 # ...26 # ...2728 protected2930 def configure_sign_up_params31 devise_parameter_sanitizer.permit(32 :sign_up,33 keys: [34 # permit :invitation_token pada user_params35 :full_name, :date_of_birth, :locale, :rate, :invitation_token36 ]37 )38 end39end
Siapkan juga controller untuk Users::FriendshipsController
.
1class Users::FriendshipsController < ApplicationController2 before_action :authenticate_user!34 def index5 @invited_friends = InviteFriend.where(user_id: current_user.id)6 @invitation_token_url = request.base_url + '/invite/' + current_user.invitation_token7 end8end
Karena saya menggunakan Devise, maka saya memiliki current_user
.
Instance variable @invitation_token_url
nanti akan diguanakan pada view template.
Routes
Selanjutnya, saya akan menambahkan routing untuk referral link agar langsung diarahkan ke halaman registrasi (*).
1Rails.application.routes.draw do2 # ...3 # ...4 # ...5 get 'referral=:invitation_token' => 'users/registrations#new'6end
Bentuk dari url dapat teman-teman sesuaikan sendiri.
View Template
Yang perlu dikerjakan pada view template ada dua bagian.
Pertama, bagian halaman registrasi.
Kedua, bagian halaman user profile yang akan menampilkan tabel yang berisi daftar teman.
Saya akan mulai dari bagian pertama.
Halaman Registrasi
Pada view template, saya perlu untuk menampung nilai instance variable @invitation_token
yang pada users_controller#new
berisi params[:user][:invitation_token]
.
1<%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>2 ...3 ...45 <div class="input-group">6 <%= f.hidden_field :invitation_token, value: @invitation_token %>7 </div>89 <%= f.submit "Sign Up", class: "btn btn-primary btn-block" %>1011 ...12 ...13<% end %>
INFO Path yang saya gunakan di atas juga merupakan path milik Devise.
User Profile Menu
Bagian kedua, tampilan frontend untuk user yang mengundang.
Paga bagian ini akan terdapat referral link yang berisi invitation_token yang akan digunakan oleh user untuk mengundang teman.
Selain itu, juga akan terdapat tabel yang menunjukkan siapa-siapa saja teman yang sudah diundang.
Sebelumnya, saya sudah menyimpakan controller dengan action index untuk user friendships_controller ini di atas (pada bagian controller).
1...2...34<!-- Invitation Token -->5<div class="mb-2 mb-sm-0">6 <text id="invitation-token">7 <%= @invitation_token_url %>8 </text>9</div>10<div class="d-flex align-items-center">11 <span id="copied"></span>12 <span id="copy-link" onclick="copyToClipboard('invitation-token')">13 Copy Link14 </span>15</div>16<!-- END Invitation Token -->1718<!-- Friendship list -->19<div class="row">20 <table class="table">21 <thead>22 <tr>23 <td>Name</td>24 <td>Joined Date</td>25 <td>Commission</td>26 </tr>27 </thead>28 <tbody>29 <% @invited_friends.each do |friend| %>30 <tr>31 <td width="60%">32 <%= image_tag(friend.friend.image_url || "avatar/profile.png", class: "rounded-circle") %>33 <%= friend.friend.full_name %>34 </td>35 <td>36 <%= friend.friend.created_at.strftime("%d %b %Y") %>37 </td>38 <% if friend&.status == 'pending' %>39 <td class="text-warning">Pending</td>40 <% elsif friend&.status == 'approved' %>41 <td class="text-primary">RM20</td>42 <% elsif friend&.status == 'rejected' %>43 <td class="text-danger">Rejected</td>44 <% end %>45 </tr>46 <% end %>47 </tbody>48 </table>49</div>
Saya menambahkan fungsi “Copy URL” agar lebih praktis, tinggal tekan tombol dan referral link akan tercopy ke dalam clipboard.
1...2...3<!-- END Friendship list -->45<script>6 // For Copy Link7 function copyToClipboard(id) {8 var from = document.getElementById(id);9 var range = document.createRange();10 window.getSelection().removeAllRanges();11 range.selectNode(from);12 window.getSelection().addRange(range);13 document.execCommand('copy');14 window.getSelection().removeAllRanges();15 };1617 // For Notification Text "Copied!" After copy link button clicked18 $('body').on("click", "#copy-link", function() {19 notification("<%= t('user.copied') %>!", 5000)20 });21 function notification(s, time) {22 $("<span class='text-primary font-family-medium'>"+s+"</span>").appendTo('#copied').fadeTo(time, 1, function() {23 $(this).fadeTo(1000, 0, function() {24 $(this).remove()25 });26 });27 };28</script>
Kekurangan(*)
Kekurangan dari fitur yang saya buat ini adalah dalam hal user experience (UX).
Karena ketika user menerima referral link, user langsung disuguhkan dengan halaman registrasi. Hal ini menjadi kekurangan, karena user tidak dapat melakukan eksplorasi pada halaman-halaman di web terlebih dahulu.
Pesan Penulis
Oke, saya akhiri catatan ini.
Mudah-mudahan sedikit banyak dapat bermanfaat buat teman-teman yang memerlukan.
Terima kasih
(^_^)
Referensi
-
guides.rubyonrails.org/association_basics.html
Diakses tanggal: 2020/02/10 -
medium.com/@carlescliment/about-rails-concerns-a6b2f1776d7d
Diakses tanggal: 2020/02/10