, Štefan Húska

Jedno kliknutie namiesto formulára: ako sme zjednodušili objednávkový flow v Rails e-shope

Konverzia e-shopu stojí a padá na počte krokov medzi “chcem” a “robím”. V našom fotolabe Tlačenica.sk musel zákazník pred nahratím fotiek najprv vybrať rozmer a typ papiera vo formulári — krok, ktorý väčšina ľudí preskočila s defaultnou voľbou (10 × 15 cm, pololesklý).

Rozhodli sme sa ten krok úplne odstrániť. Výsledok: jedno kliknutie na “Začať objednávku” a zákazník je rovno na upload stránke.

Pôvodný flow vs. nový

Pôvodne to vyzeralo takto: klik na CTA → (vytvorenie guest usera) → formulár s konfiguráciou tlače → submit → upload fotiek.

FireShot Capture 014 - Tlačenica - Profesionálny fotolab Senec - [localhost] 1.png

Nový flow: klik na CTA → upload fotiek. Jeden krok. Guest user, Album s default konfiguráciou aj košík sa vytvoria na pozadí v jednom POST requeste.

FireShot Capture 014 - Tlačenica - Profesionálny fotolab Senec - [localhost] 2.png

Unified controller action

Kľúčová zmena žije v GuestSessionsController. Pôvodne controller len vytvoril guest usera a presmeroval na formulár. Teraz robí všetko naraz — guest session, Album, košík — a pošle zákazníka priamo na upload:

O tom ako sme implementovali guest usera v devise si prečítaj v samostatnom článku.

# app/controllers/guest_sessions_controller.rb
class GuestSessionsController < ApplicationController
  rate_limit to: 10, within: 1.minute, only: :create

  def create
    unless user_signed_in?
      user = User.create_guest!
      sign_in(user)
    end

    album = current_user.albums.create!
    cart = current_user.cart || current_user.create_cart!
    cart.add_album(album.id)

    redirect_to dashboard_album_media_items_path(album)
  end
end

albums.create! bez parametrov funguje vďaka existujúcemu callbacku v Album modeli, ktorý automaticky priradí default PrintPrice:

# app/models/album.rb
before_validation :set_default_print_price, on: :create

def set_default_print_price
  return if print_price_id.present?
  default = PrintPrice.active.find_by(default: true) || PrintPrice.active.first
  self.print_price = default if default
end

Žiadna nová logika v modeli nebola potrebná — stačilo využiť to, čo tam už bolo, ale čo doteraz slúžilo len ako fallback.

POST link namiesto GET

Pôvodne boli na landing pages dva rôzne elementy: link_to pre prihlásených a button_to pre anonymných. Teraz je to jeden unified link pre všetkých:

<%= link_to 'Začať objednávku', start_order_path,
      data: { turbo_method: :post }, rel: 'nofollow', class: 'btn-primary' %>

Turbo renderuje <a> tag (rovnaký UX ako GET link), ale submit ide cez POST. To rieši tri problémy naraz:

  1. CSRF ochrana blokuje botov
  2. Turbo neprefetchuje POST linky na hover
  3. rel="nofollow" odradí crawlery. Kombinácia s rate_limit (Rails 8.1 built-in) poskytuje dostatočnú ochranu bez CAPTCHy.

Čistý mobile layout s content_for

Keď stránka obsahuje sticky bottom bar (summary bar s cenou, alebo formulár s uložením), klasický footer je zbytočný a zaberá miesto. Namiesto rastúcej podmienky v layoute (unless controller_name == "media_items" && action_name == "index") sme zaviedli deklaratívny prístup:

<%# In any partial with a sticky bar: %>
<% content_for :hide_footer, true %>

<%# In the layout: %>
<%= render 'layouts/footer' unless content_for?(:hide_footer) %>

Každý partial so sticky barom si sám deklaruje, že footer nepotrebuje. Layout nemusí vedieť o žiadnych controlleroch.

Summary bar ako navigačný prvok

Po odstránení formulárového kroku sme museli zabezpečiť, že zákazník stále vidí akú konfiguráciu má zvolenú a vie ju zmeniť.

FireShot Capture 012 - Výber fotiek - [localhost].png

Riešenie: riadok v sticky summary bare (pr. “10 × 15 cm — Pololesklý”) je tapovateľný link na edit stránku:

<%= link_to edit_dashboard_event_path(@event), data: { turbo_frame: '_top' },
            class: "... #{'config-glow' if total_pieces == 0}" do %>
  <span><%= pp.print_dimension&.title %> &mdash; <%= pp.paper_type&.title %></span>
  <span>&rsaquo;</span>
<% end %>

turbo_frame: '_top' je dôležitý — summary bar žije vnútri turbo_frame_tag 'dashboard-summary', takže bez neho by link hľadal obsah v rámci frame-u a dostal by “Content Missing”.

Pri prázdnom albume sa navyše spustí CSS animácia config-glow — trojitý pulz, ktorý upozorní zákazníka na zvolenú konfiguráciu:

tlacenica - glow 2.gif

.config-glow {
  color: rgb(2 132 199);
  animation: config-glow 0.8s ease-in-out 3;
}

@keyframes config-glow {
  0%, 100% { background-color: rgb(240 249 255); box-shadow: 0 0 4px rgb(56 189 248 / 0.1); }
  50%      { background-color: rgb(186 230 253); box-shadow: 0 0 12px rgb(56 189 248 / 0.4); }
}

Animácia prebehne len raz (forwards) a len pri prvom príchode na prázdny album — presne ten moment, keď zákazník potrebuje vedieť, čo má nastavené.

Čo si z toho odniesť

Najväčší win nebol technický, ale produktový: zbavili sme sa kroku, ktorý 90 % zákazníkov prechádzalo s defaultom. Technicky to znamenalo využiť existujúci model callback namiesto písania novej logiky, zjednotiť dva rôzne CTA elementy do jedného POST linku cez Turbo, a presunúť sekundárne akcie (zmena rozmeru, vymazanie) tam, kde neprekážajú primárnemu flow. Ak máte vo svojom checkout flow krok, ktorým väčšina ľudí prechádza bez zmeny — zvážte, či tam vôbec musí byť.