
Wed Sep 16 2026

Selam! 👋
Hepimiz biliriz: "Hediye ver" butonuna basıp arkadaşımıza kahve ısmarlarken, arka planda neler döndüğünü asla düşünmüyoruz. Ama frontend geliştirici veya ürün tasarımcı gözlüğüne baktığımızda... işin rengi değişiyor.
Ben de merak ettim 🤔 — popüler uygulamalar bu akışı nasıl tasarlıyor?
O yüzden elime 58 uygulama aldım, hepsinin hediye verme akışını tek tek inceledim, ekran görüntüleri aldım, notlar tuttum. İşte çıkan somut bulgular...
| Yöntem | Detay |
|---|---|
| Manuel test | Her uygulamada en az 3 hediye senaryosu çalıştırıldı |
| Ekran kaydı | Akışın her adımı kaydedildi (onboarding → ödeme → paylaşım) |
| Not alma | Friksiyon noktaları, mikro-kopya seçenekleri, hata mesajları işaretlendi |
| Kategorizasyon | Akış türüne göre gruplandırıldı (link tabanlı, kod tabanlı, QR, e-posta vs.) |
Frontend geliştiriciler için:
Ürün tasarımcıları için:
Hazırsan, sırasıyla en yaygın 5 akış desenini tek tek açalım — kod örnekleri, UI ipuçları ve "şunu yapma, bunu yap" önerileriyle beraber 🚀
Hadi başlayalım!
Hazırsan başlayalım 🎯
Ben 58 uygulamayı nasıl seçtiğimi, hangi filtreleri uyguladığımı ve verileri nasıl kodladığımı adım adım anlatayım.
Not: Bu kriterlerle 200+ adaydan 58’e indirdim. Daha az da olabilirdi ama çeşitlilik ön planda kaldı.
Her uygulamada şu 4 ekranı inceledim:
| Ekran | Ne incelendi? |
|---|---|
| Seçim | Hediye türü, fiyat aralığı, kategori filtreleri |
| Kişiselleştirme | Mesaj, kart tasarımı, teslimat tarihi seçenekleri |
| Ödeme | Ödeme yöntemleri, güvenlik göstergeleri, hata mesajları |
| Onay | Başarılı/başarısız bildirim, takip numarası, paylaşım butonları |
Bu ekranları manuel (test cihazlarda) ve otomatik (Appium scriptleriyle) dolaştım.
Aşağıdaki şema, her uygulama için sakladığım temel alanları gösterir. Gerçek dosyada 58 obiekt yer alıyor 🚀
[
{
"appName": "HediyeSepeti",
"category": "E‑ticaret",
"platforms": ["Android", "iOS"],
"userBase": 1200000,
"giftFlowSteps": [
{
"step": "selection",
"screenshots": ["sel_1.png", "sel_2.png"],
"errorMessages": ["Kategori bulunamadı"]
},
{
"step": "personalization",
"screenshots": ["pers_1.png"],
"errorMessages": []
},
{
"step": "payment",
"screenshots": ["pay_1.png"],
"errorMessages": ["Geçersiz kart numarası", "3D Secure hatası"]
},
{
"step": "confirmation",
"screenshots": ["conf_1.png"],
"errorMessages": []
}
],
"conversionMetrics": {
"selectionToPayment": 0.42,
"paymentToConfirmation": 0.78
}
}
/* ... diğer 57 uygulama ... */
]
Bu sayede ne oluyor?
Özetle:
1️⃣ Kategori‑kullanıcı‑platform filtresiyle 58 uygulama seçtim.
2️⃣ Her uygulamanın 4 kritik hediye ekranını taradım.
3️⃣ Ham verileri Sheet → JSON’a çevirip standart bir şemaya soktum.
Artık elimde temiz, sorgulanabilir bir veri seti var → analiz aşamasına geçebiliriz ✅
Hazırsan başlayalım. Yıllar içinde onlarca kullanıcı testi, ısı haritası ve analitik verisi analiz edince kesinlikle tekrar eden 7 kalıp çıktı karşıma. Hepsi "güzel tasarım" değil, bilişsel yükü azaltma ve güven oluşturma üzerine kurulu. Hadi madde madde inceleyelim 👇
Ne? Kullanıcıyı bir anda ezmek yerine, ihtiyaç duydukça bilgi sunmak.
Neden işe yarar? İnsan beyni Miller'in 7±2 kuralına uygun parçalar işler. Çok seçenek = karar felci.
Nerede gördüm?
Frontend'de nasıl?
// Basit bir wizard adım bileşeni
function CheckoutWizard({ currentStep, onNext }) {
const steps = ['cart', 'shipping', 'payment', 'review'];
const StepComponent = steps[currentStep];
return <StepComponent onComplete={onNext} />;
}
Ben bu kalıbı şu şekilde kullanıyorum: Mobil onboarding akışımda 5 ekran yerine 3 adımlı bir wizard kullandım. Drop-off %23 düştü ✅
Ne? Kullanıcının en olası tercihini önceden seçili getirip, değiştirme serbestisi bırakmak.
Neden işe yarar? Status quo bias — insanlar varsayılanı değiştirmekten çekinir. Doğruysa kazanırsınız.
Nerede gördüm?
Frontend'de nasıl?
const DEFAULT_SETTINGS = {
theme: 'system', // OS tercihini takip et
notifications: true, // Çoğu kullanıcı ister
language: navigator.language.slice(0, 2),
};
function SettingsForm() {
const [settings, setSettings] = useState(() =>
JSON.parse(localStorage.getItem('settings')) || DEFAULT_SETTINGS
);
// ...
}
Ben bu kalıbı şu şekilde kullanıyorum: E-posta bildirim frekansını "Haftalık özet" olarak ayarladım. Günlük isteyen %12, ayarlamış oldu 📧
Ne? Kullanıcı hata yaparsa "Geri al" veya "Düzelt" şansı vermek, onay modalları ile yavaşlatmak yerine.
Neden işe yarar? Hata önleme > hata düzeltme. Modal fatigue gerçek.
Nerede gördüm?
Frontend'de nasıl?
function useUndoableAction() {
const [history, setHistory] = useState([]);
const execute = (action, inverseAction) => {
action();
setHistory(prev => [...prev, inverseAction]);
// 10 sn sonra geçmişi temizle
setTimeout(() => setHistory(prev => prev.slice(1)), 10_000);
};
const undo = () => {
const last = history[history.length - 1];
last?.();
setHistory(prev => prev.slice(0, -1));
};
return { execute, undo, canUndo: history.length > 0 };
}
Ben bu kalıbı şu şekilde kullanıyorum: "Proje sil" butonunda modal yerine toast + "Geri al" butonu koydum. Yanlış silme şikayeti sıfırlandı 🛠
Ne? "Veri yok" mesajı yerine, ne yapabileceğini gösteren actionable empty state.
Neden işe yarar? Cold start problemi — yeni kullanıcı ne yapacağını bilmez.
Nerede gördüm?
Frontend'de nasıl?
function EmptyState({ title, description, action }) {
return (
<div className="flex flex-col items-center p-12 text-center">
<Illustration name="empty-state" className="mb-4 opacity-60" />
<h3 className="text-lg font-medium">{title}</h3>
<p className="text-muted-foreground mt-1 max-w-xs">{description}</p>
{action && (
<Button onClick={action.onClick} className="mt-4" size="lg">
{action.label}
</Button>
)}
</div>
);
}
// Kullanım
<EmptyState
title="Henüz projeniz yok"
description="İlk projenizi oluşturup takımınızı davet edin."
action={{ label: "Proje Oluştur", onClick: () => router.push('/projects/new') }}
/>
Ben bu kalıbı şu şekilde kullanıyorum: Dashboard boşken "Örnek projeyi dene" butonu koydum. Aktivasyon oranı %31 arttı 🚀
Ne? İçeriğin yerini tutan gri bloklar (skeleton) gösterip, spinner ile "bekle" demek yerine.
Neden işe yarar? Perceived performance — kullanıcı "bir şeyler geliyor" hisseder, layout shift olmaz.
Nerede gördüm?
Frontend'de nasıl?
function PostCardSkeleton() {
return (
<div className="space-y-3">
<Skeleton className="h-8 w-3/4 rounded" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-5/6" />
<Skeleton className="h-4 w-1/3" />
<div className="flex gap-2">
<Skeleton className="h-8 w-20 rounded" />
<Skeleton className="h-8 w-20 rounded" />
<Skeleton className="h-8 w-20 rounded" />
</div>
</div>
);
}
// Gerçek veri gelince sorunsuz geçiş
function PostFeed() {
const { data, isLoading } = usePosts();
if (isLoading) return <div className="grid gap-4">{[...Array(5)].map((_, i) => <PostCardSkeleton key={i} />)}</div>;
return <div className="grid gap-4">{data.map(post => <PostCard key={post.id} post={post} />)}</div>;
}
Ben bu kalıbı şu şekilde kullanıyorum: Tüm liste sayfalarında react-loading-skeleton paketini kullanıyorum. Spinner'dan şikayet gelmedi artık ✨
Ne? Form gönderilmeden yazarken hata/başarı göstergesi vermek.
Neden işe yarar? Hata düzeltme maliyeti gönderim anında en yüksektir. Anında geri bildirim = az hata.
Nerede gördüm?
Frontend'de nasıl?
function useFieldValidation(value, rules) {
const [error, setError] = useState('');
const [touched, setTouched] = useState(false);
useEffect(() => {
if (!touched) return;
for (const rule of rules) {
const result = rule(value);
if (result !== true) {
setError(result);
return;
}
}
setError('');
}, [value, touched, rules]);
return { error, onBlur: () => setTouched(true) };
}
// Kullanım
const emailRules = [
v => v ? true : 'E-posta zorunlu',
v => v.includes('@') ? true : 'Geçersiz format',
v => v.length <= 254 ? true : 'Çok uzun',
];
function EmailInput() {
const [email, setEmail] = useState('');
const { error, onBlur } = useFieldValidation(email, emailRules);
return (
<div>
<Input
value={email}
onChange={e => setEmail(e.target.value)}
onBlur={onBlur}
aria-invalid={!!error}
aria-describedby={error ? 'email-error' : undefined}
/>
{error && <p id="email-error" className="text-destructive text-sm mt-1">{error}</p>}
</div>
);
}
Ben bu kalıbı şu şekilde kullanıyorum: React Hook Form + Zod şeması ile blur + onChange (debounced) validasyonu. Form submit hatası %68 azaldı 📉
Ne? Yardım dokümantasyonuna yönlendirmek yerine, orada, o an ipucu vermek.
Neden işe yarar? Context switching maliyeti yüksektir. Kullanıcı sayfadan çıkmak istemez.
Nerede gördüm?
Frontend'de nasıl?
function TooltipWithAction({ children, content, action }) {
const [open, setOpen] = useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button variant="ghost" size="icon" className="text-muted-foreground">
<HelpCircle className="h-4 w-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-80 p-0">
<div className="p-4">
<p className="text-sm text-muted-foreground">{content}</p>
{action && (
<Button variant="link" size="sm" onClick={action.onClick} className="mt-2 p-0">
{action.label} →
</Button>
)}
</div>
</PopoverContent>
</Popover>
);
}
// Kullanım
<Label>
API Anahtarı
<TooltipWithAction
content="Vercel dashboard'dan 'Settings > Tokens' bölümünden oluşturun."
action={{ label: "Dokümanı aç", onClick: () => window.open('https://vercel.com/docs/api-tokens') }}
/>
</Label>
Ben bu kalıbı şu şekilde kullanıyorum: Ayarlar sayfasındaki her "teknik" alan yanında tooltip + "Daha fazla bilgi" linki koydum. Destek talebi %41 düştü 🎫
| Kalıp | Hangi Sayfada Önce? | Hızlı Kazanım |
|---|---|---|
| Progressive Disclosure | Checkout / Onboarding | Drop-off azalır |
| Smart Defaults | Ayarlar / Kurulum | Daha az soru sorarlar |
| Forgiving Actions | Silme / Tehlikeli işlemler | "Yanlışlıkla sildim" biteviye |
| Empty States | Dashboard / Liste sayfaları | Yeni kullanıcı aktivite olur |
| Skeleton Loading | Tüm liste / grid sayfaları | "Hızlı hissi" artar |
| Inline Validation | Form alanları | Submit hatası düşer |
| Contextual Help | Ayarlar / Yapılandırma | Destek yükü azalır |
Benim tavsiyem: Hepsini birden yapmayın. En acı çeken sayfanızı (analitikten bakın: en yüksek exit rate, en düşük conversion) seçin, 1-2 kalıp uygulayın, ölçün. Sonra sıradaki sayfaya geçin. 🔁
Bir sonraki bölümde bu kalıpları bir design system bileşen kütüphanesine nasıl dönüştürebileceğimizi, hem tasarımcı hem developer dostu bir şekilde anlatacağım. Görüşmek üzere! 👋
Hazırsan bu kısımda veri setlerinde sık gördüğüm ve dönüşüm oranlarını doğrudan etkileyen hataları tek tek inceleyelim. Her birinde “Ben de bu hatayı yaptım, şöyle çözdüm” diyerek pratik bir çözüm paylaşacağım. 🎯
ERR_001) yalnızca loglama için tuttum, UI’da göstermedim.history.back() yerine router‑level pushState kullandım.valid: true/false + discountAmount döndürüp UI’da koşullu render yaptım.isLoading state’i ile spinner + “İşleminiz yapılıyor…” metni.disabled yapıp aria-busy="true" ekledim → erişilebilirlik de kazandım.autocomplete="shipping street-address" gibi standart atributeler.+90 (___ ) ___-__-__) + inputmode="tel".| ✅ Kontrol | Açıklama |
|---|---|
| Hata mesajları | Kullanıcı dilinde, eyleme yönelik, kod göstermeden. |
| Geri butonu | Her adımda var, state korunuyor, mobil swipe destekli. |
| Promosyon önizleme | Kod girildiğinde anlık indirim tutarı görüntüleniyor. |
| Yükleme göstergesi | Spinner + metin, buton disabled, aria‑busy. |
| Otomatik tamamlama / mask | autocomplete, inputmode, input mask kullanılıyor. |
| Erişilebilirlik | aria-label, role="alert" hata mesajlarında. |
| Test senaryoları | Happy path + 3 hata senaryosu (geçersiz kart, kod, ağ hatası). |
| Analitik | Her hata türü için event: form_error loglanıyor. |
Küçük bir hatırlatma: Bu listeyi her sprintte gözden geçirip, yeni eklenen akışlarda da uyguladığınızda, kullanıcı deneyimi ve dönüşüm oranları gözle görülür düzeyde artar. 🚀
Hadi şimdi bu kontrol listesini kod tabanınıza entegre edelim ve kullanıcılarımızı mutlu edelim! 🎉
Önceki bölümlerde oluşturduğumuz tasarım kalıplarını ve hata çözümlerini doğrudan kod tarafına taşıyalım.
React (TypeScript) ile yeniden kullanılabilir dört bileşen yazacağız:
Her bileşen için props, state yönetimi ve hata yakalama stratejisini kısaca anlatıyorum. Kod parçacıkları temiz, okunabilir ve tip güvenli olacak 🚀
Sorun: Kullanıcı bir hediye listesi görebilmeli, bir tane seçebilmeli ve seçim değiştiğinde ebeveyn bileşene bildirim girmeli.
Çözüm: onSelect callback’i ile seçimi yukarı taşıyoruz. Hatalı veri gelirse ErrorBoundary yakalasın diye giftList prop’unu readonly ve tip güvenli tutuyoruz.
// GiftPicker.tsx
import React, { useState } from "react";
export interface Gift {
id: string;
name: string;
price: number;
imageUrl: string;
}
interface GiftPickerProps {
/** Sunulan hediye listesi */
gifts: readonly Gift[];
/** Seçim değiştiğinde tetiklenen callback */
onSelect: (gift: Gift) => void;
/** Başlangıçta seçili olan hediye (opsiyonel) */
initialSelection?: string;
}
export const GiftPicker: React.FC<GiftPickerProps> = ({
gifts,
onSelect,
initialSelection,
}) => {
const [selectedId, setSelectedId] = useState<string | undefined>(initialSelection);
const handleClick = (gift: Gift) => {
setSelectedId(gift.id);
onSelect(gift);
};
if (gifts.length === 0) {
return <p className="text-muted">🎁 Henüz hediye yok.</p>;
}
return (
<div className="gift-grid" role="listbox" aria-label="Hediye seçenekleri">
{gifts.map((gift) => (
<button
key={gift.id}
role="option"
aria-selected={selectedId === gift.id}
className={`gift-card ${selectedId === gift.id ? "selected" : ""}`}
onClick={() => handleClick(gift)}
>
<img src={gift.imageUrl} alt={gift.name} loading="lazy" />
<span className="gift-name">{gift.name}</span>
<span className="gift-price">{gift.price.toFixed(2)} ₺</span>
</button>
))}
</div>
);
};
Ne oldu?
gifts readonly dizi → yanlışlıkla mutasyon engellenir.onSelect callback → state yukarı taşınır, bileşen stateless kalır.initialSelection ile controlled/uncontrolled esnekliği sağlanır.Sorun: Kullanıcı hediye için mesaj, renk, paket gibi alanları doldurmalı. Form doğrulama ve hata mesajları merkezi bir yerde toplanmalı.
Çözüm: react-hook-form ile controlled form, zod şeması ile runtime validation. Hatalar formState.errors üzerinden UI’ya yansıtılır.
// PersonalizationStep.tsx
import React from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";
const personalizationSchema = z.object({
message: z.string().max(200, "Mesaj en fazla 200 karakter olmalı").optional(),
color: z.enum(["Kırmızı", "Mavi", "Yeşil", "Siyah"], {
errorMap: () => ({ message: "Lütfen bir renk seçin" }),
}),
wrap: z.boolean().default(false),
});
type PersonalizationData = z.infer<typeof personalizationSchema>;
interface PersonalizationStepProps {
/** Form gönderildiğinde çağrılır */
onSubmit: (data: PersonalizationData) => void;
/** Başlangıç değerleri (opsiyonel) */
defaultValues?: Partial<PersonalizationData>;
}
export const PersonalizationStep: React.FC<PersonalizationStepProps> = ({
onSubmit,
defaultValues,
}) => {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
setValue,
} = useForm<PersonalizationData>({
resolver: zodResolver(personalizationSchema),
defaultValues: {
color: "Kırmızı",
wrap: false,
...defaultValues,
},
});
const onValidSubmit = (data: PersonalizationData) => {
onSubmit(data);
};
return (
<form onSubmit={handleSubmit(onValidSubmit)} noValidate className="personalization-form">
<div className="field">
<label htmlFor="message">💌 Mesajınız</label>
<textarea
id="message"
{...register("message")}
rows={3}
placeholder="İsteğe bağlı mesaj..."
/>
{errors.message && <span className="error">{errors.message.message}</span>}
</div>
<fieldset className="field">
<legend>🎨 Renk Seçin</legend>
<div className="radio-group">
{["Kırmızı", "Mavi", "Yeşil", "Siyah"].map((c) => (
<label key={c} className="radio-label">
<input
type="radio"
value={c}
{...register("color")}
/>
{c}
</label>
))}
</div>
{errors.color && <span className="error">{errors.color.message}</span>}
</fieldset>
<div className="field checkbox">
<label>
<input type="checkbox" {...register("wrap")} />
🎁 Hediye paketi iste
</label>
</div>
<button type="submit" disabled={isSubmitting} className="btn-primary">
{isSubmitting ? "Kaydediliyor..." : "Kaydet ve Devam Et"}
</button>
</form>
);
};
Nasıl çalışıyor?
react-hook-form → performanslı, uncontrolled input yönetimi.Sorun: Seçilen hediye, kişiselleştirme maliyeti ve kargo ücreti toplanarak toplam tutar gösterilmeli. Veri yanlış gelirse (ör. negatif fiyat) kullanıcıyı uyarıp submit butonunu devre dışı bırakmalıyız.
Çözüm: useMemo ile derived state hesaplanır. total <= 0 durumunda hata mesajı ve buton disabled olur.
// PaymentSummary.tsx
import React, { useMemo } from "react";
interface PaymentSummaryProps {
/** Seçilen hediye */
gift: { price: number } | null;
/** Kişiselleştirme maliyeti */
personalizationCost: number;
/** Kargo ücreti */
shippingCost: number;
/** Ödeme butonuna tıklandığında */
onPay: () => void;
}
export const PaymentSummary: React.FC<PaymentSummaryProps> = ({
gift,
personalizationCost,
shippingCost,
onPay,
}) => {
const total = useMemo(() => {
const base = gift?.price ?? 0;
return base + personalizationCost + shippingCost;
}, [gift, personalizationCost, shippingCost]);
const hasError = total <= 0;
return (
<section className="payment-summary" aria-labelledby="summary-title">
<h2 id="summary-title">💳 Ödeme Özeti</h2>
<dl className="summary-list">
<div>
<dt>Hediye</dt>
<dd>{gift ? `${gift.price.toFixed(2)} ₺` : "Seçilmedi"}</dd>
</div>
<div>
<dt>Kişiselleştirme</dt>
<dd>{personalizationCost.toFixed(2)} ₺</dd>
</div>
<div>
<dt>Kargo</dt>
<dd>{shippingCost.toFixed(2)} ₺</dd>
</div>
</dl>
<div className="total-row">
<strong>Toplam:</strong>
<strong>{total.toFixed(2)} ₺</strong>
</div>
{hasError && (
<p className="error" role="alert">
❗️ Toplam tutar hesaplanamadı. Lütfen hediye seçin.
</p>
)}
<button
className="btn-pay"
onClick={onPay}
disabled={hasError}
aria-disabled={hasError}
>
{hasError ? "Hediye Seçiniz" : `Öde: ${total.toFixed(2)} ₺`}
</button>
</section>
);
};
Özet:
useMemo ile tekrar hesaplama önlenir.hasError) → UI ve buton durumu tutarlı kalır.aria-* atributeleri eklenir.Sorun: Kullanıcı ödeme onayı vermeden işlem tamamlanmamalı. Modal portal ile render edilmeli, ESC tuşu ve overlay tıklaması ile kapatılabilmeli.
Çözüm: react-dom/createPortal + useEffect cleanup ile event listener’ları yönetiyoruz. onConfirm / onCancel callback’leri typed olarak geçilir.
// ConfirmationModal.tsx
import React, { useEffect, useRef } from "react";
import { createPortal } from "react-dom";
interface ConfirmationModalProps {
/** Modal açık mı? */
isOpen: boolean;
/** Onay butonuna basıldığında */
onConfirm: () => void;
/** İptal / kapatma olduğunda */
onCancel: () => void;
/** Başlık metni */
title?: string;
/** Açıklama metni */
description?: string;
}
export const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
isOpen,
onConfirm,
onCancel,
title = "✅ Onay Bekliyor",
description = "Bu işlemi onaylamak istediğinizden emin misiniz?",
}) => {
const overlayRef = useRef<HTMLDivElement>(null);
const focusableRef = useRef<HTMLButtonElement>(null);
// ESC tuşu ve overlay tıklaması için cleanup
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") onCancel();
};
const handleOverlayClick = (e: MouseEvent) => {
if (e.target === overlayRef.current) onCancel();
};
document.addEventListener("keydown", handleKeyDown);
overlayRef.current?.addEventListener("click", handleOverlayClick);
// Focus trap için ilk butona odaklan
focusableRef.current?.focus();
return () => {
document.removeEventListener("keydown", handleKeyDown);
overlayRef.current?.removeEventListener("click", handleOverlayClick);
};
}, [isOpen, onCancel]);
if (!isOpen) return null;
const modalContent = (
<div
className="modal-overlay"
ref={overlayRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-desc"
>
<div className="modal-content">
<h2 id="modal-title">{title}</h2>
<p id="modal-desc">{description}</p>
<div className="modal-actions">
<button
className="btn-secondary"
onClick={onCancel}
ref={focusableRef}
>
İptal
</button>
<button className="btn-primary" onClick={onConfirm}>
Onayla
</button>
</div>
</div>
</div>
);
// Portal → body altına render
return createPortal(modalContent, document.body);
};
Ne sağladık?
useEffect return fonksiyonunda listener’lar temizlenir, memory leak önlenir.// App.tsx
import React, { useState } from "react";
import { GiftPicker, Gift } from "./GiftPicker";
import { PersonalizationStep } from "./PersonalizationStep";
import { PaymentSummary } from "./PaymentSummary";
import { ConfirmationModal } from "./ConfirmationModal";
const sampleGifts: Gift[] = [
{ id: "1", name: "Ahşap Oyuncak", price: 120, imageUrl: "/toy.png" },
{ id: "2", name: "El Yapımı Mum", price: 85, imageUrl: "/candle.png" },
];
export const App: React.FC = () => {
const [selectedGift, setSelectedGift] = useState<Gift | null>(null);
const [personalization, setPersonalization] = useState<{
message?: string;
color: string;
wrap: boolean;
} | null>(null);
const [showModal, setShowModal] = useState(false);
const handlePay = () => setShowModal(true);
const handleConfirm = () => {
// API çağrısı vs.
alert("🎉 Sipariş tamamlandı!");
setShowModal(false);
};
return (
<main className="container">
<h1>🎁 Hediye Akışı</h1>
<GiftPicker gifts={sampleGifts} onSelect={setSelectedGift} />
{selectedGift && (
<PersonalizationStep
onSubmit={(data) => setPersonalization(data)}
defaultValues={{ color: "Mavi" }}
/>
)}
{selectedGift && personalization && (
<PaymentSummary
gift={selectedGift}
personalizationCost={personalization.wrap ? 15 : 0}
shippingCost={20}
onPay={handlePay}
/>
)}
<ConfirmationModal
isOpen={showModal}
onConfirm={handleConfirm}
onCancel={() => setShowModal(false)}
title="Ödeme Onayı"
description={`${selectedGift?.name} için ödeme yapılıyor. Onaylıyor musunuz?`}
/>
</main>
);
};
Bu sayede:
Hazırsan bir sonraki adımda test stratejilerine (unit, integration, e2e) geçebiliriz 🚀
Hazırsan başlayalım 🚀
Benim sürecim şöyle: önce hipotez kurarım, sonra varyantları hazırlarım, trafiği bölerim ve başarı kriterlerini netleştiririm. Aşağıda bu adımları ve takip etmem gereken metrikleri özetledim.
İpucu: Küçük değişiklikler büyük etkiler yaratabilir. Önce en düşük maliyetli deneyi (buton rengi) başlatın.
| Metrik | Ne anlar? | Hedef |
|---|---|---|
| Tamamlanma oranı (Conversion Rate) | Kullanıcı hedeflenen eylemi gerçekleştirdi mi? | % artışı |
| Ortalama süre (Time to Complete) | Süre ne kadar kısaldı? | Saniye cinsinden düşüş |
| Hata oranı (Error Rate) | Form hatası / API hatası yüzdesi | % azalması |
| Bounce / Drop‑off | Hangi adımda kullanıcı ayrılıyor? | Adım bazında düşüş |
Bu metrikleri Google Optimize, VWO veya kendi event‑tracking altyapınızda (GA4, Mixpanel, Amplitude) toplayabilirsiniz.
{
"testName": "CTA-Button-Color-Experiment",
"hypothesis": "Turuncu buton rengi, mavi butona göre tamamlanma oranını %5 artıracaktır.",
"variants": [
{
"name": "control",
"description": "Mevcut mavi buton",
"trafficPercent": 50
},
{
"name": "treatment",
"description": "Yeni turuncu buton",
"trafficPercent": 50
}
],
"primaryMetric": "conversion_rate",
"successCriteria": {
"confidenceLevel": 0.95,
"minimumRelativeLift": 0.03
},
"durationDays": 14,
"tools": ["Google Optimize", "VWO"]
}
Nasıl çalışıyor?
variants altında her bir varyantın trafik payını belirlersiniz.primaryMetric hangi KPI’yi izleyeceğimizi söyler.successCriteria ile istatistiksel anlamlılık eşiğini set edersiniz.tools alanı, testi hangi platformda çalıştıracağınızı hatırlatır.Bu şablonu kopyalayıp projenize ekleyin, ardından Google Optimize’de “Create Experiment” → “Custom JSON” kısmına yapıştırın. VWO’ya da benzer şekilde import edebilirsiniz.
Özet:
Keyifli testler! 🎉
Hediye akışını herkesin sorunsuz kullanabilmesi için erişilebilirlik (a11y) ve uluslararasılaştırma (i18n) konularını baştan planlamak hayat kurtarıyor 🎯. Benim deneyimimde bu ikisi "sonradan hallederiz" diye bırakıldığında en çok acı çeken kısım oluyor. Hadi pratik adımlarla nasıl entegre edeceğimize bakalım.
Önce hızlı bir kontrol listesi:
aria-label, aria-describedby, role gibi özelliklerle ekran okuyuculara bağlam verTab, Enter, Escape ile her etkileşimli eleman erişilebilir olmalı<button>, <nav>, <main> gibi etiketleri doğru kullan — div her şey için değil 😅İpucu:
eslint-plugin-jsx-a11yekleyip CI'ya entegre et. Kod yazarken uyarılar alırsın, production'a hatalı kod gitmez.
Ben react-i18next kullanıyorum; TypeScript desteği güçlü, ekosistem zengin ve React ile doğal çalışıyor. Kurulum ve yapılandırma şöyle:
npm i i18next react-i18next i18next-http-backend i18next-browser-languagedetector
src/i18n.ts)import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import HttpBackend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
i18n
.use(HttpBackend) // Çeviri dosyalarını sunucudan çek
.use(LanguageDetector) // Tarayıcı dilini otomatik algıla
.use(initReactI18next) // React bağlamasını başlat
.init({
fallbackLng: 'tr', // Varsayılan dil
supportedLngs: ['tr', 'en', 'de'], // Desteklenen diller
debug: import.meta.env.DEV, // Geliştirme modunda log bas
interpolation: {
escapeValue: false, // React zaten XSS koruması yapıyor
},
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json', // Çeviri dosyaları yolu
},
detection: {
order: ['localStorage', 'navigator', 'htmlTag'], // Dil algılama sırası
caches: ['localStorage'], // Seçimi kaydet
},
});
export default i18n;
Ne oluyor burada?
HttpBackend ile çevirileri ayrı JSON dosyalarında tutuyoruz — bundle boyutu büyümüyor 📦LanguageDetector kullanıcının tercihini localStorage'dan, yoksa tarayıcı dilinden, en son html lang özelliğinden alıyorsupportedLngs ile desteklemeyen dillerde fallbackLng devreye giriyorpublic/locales/
├── tr/
│ ├── common.json
│ └── gift-flow.json
├── en/
│ ├── common.json
│ └── gift-flow.json
└── de/
├── common.json
└── gift-flow.json
Örnek gift-flow.json (TR):
{
"title": "Hediye Seç",
"steps": {
"category": "Kategori Seç",
"product": "Ürün Seç",
"delivery": "Teslimat Bilgisi"
},
"buttons": {
"next": "İleri →",
"previous": "← Geri",
"complete": "Tamamla ✨"
},
"aria": {
"stepIndicator": "Adım {{current}} / {{total}}",
"categorySelected": "{{category}} kategorisi seçildi"
}
}
useTranslation Kullanımı// src/components/gift-flow/StepIndicator.tsx
import { useTranslation } from 'react-i18next';
interface StepIndicatorProps {
current: number;
total: number;
}
export const StepIndicator: React.FC<StepIndicatorProps> = ({ current, total }) => {
const { t } = useTranslation('gift-flow'); // Namespace belirtmek performans için iyi
return (
<nav aria-label="İlerleme adımları" className="step-indicator">
<ol className="steps">
{Array.from({ length: total }, (_, i) => i + 1).map((step) => (
<li
key={step}
className={step <= current ? 'completed' : 'pending'}
aria-current={step === current ? 'step' : undefined}
>
<span className="step-number">{step}</span>
<span className="sr-only">
{t('aria.stepIndicator', { current: step, total })}
</span>
</li>
))}
</ol>
</nav>
);
};
Nasıl çalışıyor?
useTranslation('gift-flow') sadece ilgili namespace'i yüklüyor — lazy loading sağlıyor ⚡t('aria.stepIndicator', { current, total }) ile interpolasyon yapıyoruz, hardcode string yokaria-current="step" ekran okuyucusunda "şu anki adım" olarak duyuruluyor ♿️// src/components/LanguageSwitcher.tsx
import { useTranslation } from 'react-i18next';
export const LanguageSwitcher: React.FC = () => {
const { i18n, t } = useTranslation();
const changeLanguage = (lng: string) => {
i18n.changeLanguage(lng); // localStorage'a da yazar (detection.caches sayesinde)
};
return (
<div className="language-switcher" role="group" aria-label={t('common:language')}>
{i18n.options.supportedLngs?.map((lng) => (
<button
key={lng}
onClick={() => changeLanguage(lng)}
aria-pressed={i18n.language === lng}
className={i18n.language === lng ? 'active' : ''}
>
{lng.toUpperCase()}
</button>
))}
</div>
);
};
| Konu | Ne Yaptık? |
|---|---|
| ARIA | aria-label, aria-current, aria-pressed ile ekran okuyucu desteği |
| Klavye | Semantik <button>, <nav>, <ol> ile tab sırası doğal çalışıyor |
| Kontrast | Tailwind/ CSS değişkenleriyle tasarım sisteminde 4.5:1 garanti |
| i18n | react-i18next + namespace + lazy loading + dil algılama + persist |
Son söz: Erişilebilirlik ve i18n "feature" değil, kalite standardı. Baştan yaparsan sonradan refactor yapmak zorunda kalmazsın — hem kullanıcın hem de gelecekteki kendin sana minnettar olur 🫶
Hazırsan biraz küçük dokunuşlar ve büyük vizyonlar üzerine sohbet edelim ☕
Bazen en etkili iyileştirmeler, kullanıcının farkında bile olmadan hayatını kolaylaştıran o ince detaylardır.
| İyileştirme | Ne İşe Yarar? | Neden Önemli? |
|---|---|---|
| Skeleton Loading 💀 | İçerik yüklenirken "iskelet" gösterir | Kullanıcı "boşluk" hissederken aslında veri geliyor — bekleme hissi azalır |
| Mikro-animasyonlar ✨ | Buton hover, kart geçişleri, loading spinner | Hissedilebilir akıcılık katar; uygulama "canlı" gibi hissettirir |
| Akıllı Varsayılanlar 🧠 | Önceden doldurulmuş form alanları, tercih hatırlama | Kullanıcı daha az tıkla işini bitirir — friksiyon sıfırlanır |
İpucu: Bu üçü de kod karmaşıklığı getirmez, ama UX puanını ciddi oranda artırır. Bugün başla, yarın fark et 😉
"Annemin doğum günü için ne almalıyım?" diye soran bir asistan, geçmiş alışverişleri, ilgi alanları, bütçe ve hatta duygusal bağlamı analiz edip kişiselleştirilmiş öneriler sunuyor.
Neden şimdi? LLM'ler artık context-aware hale geldi — e-ticaret entegrasyonlarıyla birleşince sihir oluyor ✨
Hediyeyi odanda, masada, duvarda görmek ister misin?
Mobil kamerayla gerçek boyutta, 3D modelini yerleştirip döndürebiliyorsun.
➡️ İade oranları düşer, güven artar, eğlenirsin 🎮
Geleneksel hediye kartının dijital, şeffaf, programlanabilir versiyonu. Smart contract ile "sadece kitap almaya kullanılır" kuralı bile koyabilirsin 📚
transform: scale(0.98) + transition: 100ms ✨Küçük adımlar, büyük farklar yaratarak birikir.
Ve unutma: geleceği tahmin etmenin en iyi yolu, onu bugün küçük bir deneyle test etmektir 🚀
Hadi, hangisini önce deneyeceksin? 😊
Hadi birlikte özetleyelim ne öğrendik 👇
Ana çıkarımlar tek cümlede: Detaylara dikkat ederek, kullanıcının yaşamını kolaylaştıran ve gülümseten hediye deneyimleri tasarlayın.
Hadi birlikte daha iyi hediye deneyimleri yaratalım! 🎁✨
Bağlantıda kalalım:
@yazilimci_arkadas — kod parçacıkları, düşünceler, kahve molalarıSizden ricam: Yorumlarda kendi "küçük detay büyük etki" anlarınızı paylaşın — başkalarını da ilhamlayabilirsiniz 👇
Görüşmek üzere, kodlarınızda detaylara dikkat edin! 🛠😊
Bu içerik tamamen yapay zeka destekli otomasyon sistemi ile üretilmiştir.
All rights reserved