, Š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](/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NjIsInB1ciI6ImJsb2JfaWQifX0=--04de053be3ebb40a3865a40b868279a514744236/FireShot%20Capture%20014%20-%20Tla%C4%8Denica%20-%20Profesion%C3%A1lny%20fotolab%20Senec%20-%20%5Blocalhost%5D%201.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](/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NjMsInB1ciI6ImJsb2JfaWQifX0=--e3ddbb64b25b82bdd7f6c291ca585a47e3734995/FireShot%20Capture%20014%20-%20Tla%C4%8Denica%20-%20Profesion%C3%A1lny%20fotolab%20Senec%20-%20%5Blocalhost%5D%202.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:
- CSRF ochrana blokuje botov
- Turbo neprefetchuje POST linky na hover
rel="nofollow"odradí crawlery. Kombinácia srate_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](/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NjAsInB1ciI6ImJsb2JfaWQifX0=--9d04fa8b7b2ef9c70756e32a0ce6f9829caec7fe/FireShot%20Capture%20012%20-%20V%C3%BDber%20fotiek%20-%20%5Blocalhost%5D.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 %> — <%= pp.paper_type&.title %></span>
<span>›</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:

.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ť.