İçeriğe geç
BigBang

Sık açılanlar

    ↑↓Gezin↵AçEscKapat
    EN

    Kalıplar

    Kaydetme

    Açık kaydetme ile otomatik kaydetme arasında seçim, sonucun gösterimi ve girilen verinin korunması.

    Kullanıcı verisini kaydetmek, ürüne duyulan güvenin temelidir. Bu kalıp iki yolu tanımlar: kullanıcının düğmeyle kaydettiği form ve kendiliğinden kaydedilen belge.

    • Açık kaydetme: doğrulaması veya yan etkisi olan formlarda (ayarlar, profil, kayıt oluşturma).
    • Otomatik kaydetme: belge ve not gibi sürekli yazılan alanlarda.
    • İkisi aynı ekranda karışmaz; bir ekran tek yöntem kullanır.

    Açık kaydetmede düğme yüklenir, sonra başarı işaretini gösterir ve “Değişiklikler kaydedildi” toast’ı çıkar. Otomatik kaydetmede yazmayı bıraktıktan 1000 ms sonra kayıt başlar. Hata hikâyelerinde ilk deneme başarısız olur.

    Canlı örnek, StorybookStorybook'ta aç
    Proje ayarları

    Ekip üyeleri bu adı listede görür.

    import { Button, InlineAlert, Inline, Stack, Text, TextInput, useToast } from '@bigbang/ui';
    import { useEffect, useRef, useState, type FormEvent } from 'react';
    import { type Lang, LangProvider } from '../lang';
    const copy = {
    tr: {
    title: 'Proje ayarları',
    name: 'Proje adı',
    description: 'Ekip üyeleri bu adı listede görür.',
    save: 'Kaydet',
    saving: 'Kaydediliyor',
    saved: 'Değişiklikler kaydedildi',
    failedTitle: 'Kaydedilemedi',
    failed: 'Değişiklikler kaydedilemedi. Girdiğiniz bilgiler korundu; bağlantınızı denetleyip tekrar deneyin.',
    retry: 'Tekrar dene',
    initial: 'Proje 1',
    },
    en: {
    title: 'Project settings',
    name: 'Project name',
    description: 'Team members see this name in the list.',
    save: 'Save',
    saving: 'Saving',
    saved: 'Changes saved',
    failedTitle: 'Not saved',
    failed: 'The changes were not saved. What you entered is kept; check your connection and try again.',
    retry: 'Try again',
    initial: 'Project 1',
    },
    };
    type Status = 'idle' | 'saving' | 'saved' | 'failed';
    /** How long the success mark stays on the button. */
    export const SUCCESS_HOLD = 2000;
    type Props = { lang?: Lang; failFirst?: boolean; delay?: number };
    function Form({ lang = 'tr', failFirst = false, delay = 1200 }: Props) {
    const t = copy[lang];
    const toast = useToast();
    const [value, setValue] = useState(t.initial);
    const [status, setStatus] = useState<Status>('idle');
    const [mark, setMark] = useState(false);
    const [dirty, setDirty] = useState(false);
    const attemptsRef = useRef(0);
    const timersRef = useRef<ReturnType<typeof setTimeout>[]>([]);
    useEffect(() => {
    const pending = timersRef.current;
    return () => pending.forEach(clearTimeout);
    }, []);
    // Leaving with unsaved input asks the browser to confirm.
    useEffect(() => {
    if (!dirty) return;
    const guard = (event: BeforeUnloadEvent) => event.preventDefault();
    window.addEventListener('beforeunload', guard);
    return () => window.removeEventListener('beforeunload', guard);
    }, [dirty]);
    const save = () => {
    if (status === 'saving') return;
    setStatus('saving');
    setMark(false);
    const attempt = attemptsRef.current++;
    timersRef.current.push(
    setTimeout(() => {
    if (failFirst && attempt === 0) {
    setStatus('failed');
    return;
    }
    setStatus('saved');
    // The toast announces the result once; the button's own success announcement is off (`successLabel=""`).
    toast.show({ title: t.saved, tone: 'success' });
    setDirty(false);
    setMark(true);
    timersRef.current.push(setTimeout(() => setMark(false), SUCCESS_HOLD));
    }, delay),
    );
    };
    const onSubmit = (event: FormEvent) => {
    event.preventDefault();
    save();
    };
    return (
    <form onSubmit={onSubmit} noValidate className="w-full max-w-md" aria-labelledby={`save-title-${lang}`}>
    <Stack gap="lg">
    <Text weight="medium" id={`save-title-${lang}`}>
    {t.title}
    </Text>
    <TextInput
    label={t.name}
    description={t.description}
    name="project"
    value={value}
    onValueChange={(next) => {
    setValue(next);
    setDirty(true);
    if (status !== 'saving') setStatus('idle');
    }}
    />
    <Inline gap="md" align="center" wrap>
    <Button type="submit" loading={status === 'saving'} loadingLabel={t.saving} success={mark} successLabel="">
    {t.save}
    </Button>
    </Inline>
    {status === 'failed' ? (
    <InlineAlert
    tone="danger"
    title={t.failedTitle}
    announce
    actions={
    <Button variant="secondary" size="sm" onClick={save}>
    {t.retry}
    </Button>
    }
    >
    {t.failed}
    </InlineAlert>
    ) : null}
    </Stack>
    </form>
    );
    }
    /**
    * Explicit save: the button shows progress, then a success mark held for 2000 ms, and a success toast reports the result.
    * On failure the input stays, a danger `InlineAlert` says what happened and its "Try again" action repeats the save.
    * `failFirst` makes the first attempt fail so the failure path can be shown.
    */
    export function ExplicitSave(props: Props) {
    return (
    <LangProvider lang={props.lang ?? 'tr'}>
    <Form {...props} />
    </LangProvider>
    );
    }
    Canlı örnek, StorybookStorybook'ta aç
    import { Button, InlineAlert, Inline, Spinner, Stack, Text, Textarea } from '@bigbang/ui';
    import { useEffect, useRef, useState } from 'react';
    import { type Lang, LangProvider } from '../lang';
    const copy = {
    tr: {
    label: 'Toplantı notu',
    initial: 'Gündem: rapor 12 ve proje 3.',
    saving: 'Kaydediliyor…',
    saved: (time: string) => `Kaydedildi · ${time}`,
    failed: 'Kaydedilemedi',
    failedHint: 'Yazdığınız metin burada duruyor. Bağlantı gelince tekrar deneyin.',
    retry: 'Tekrar dene',
    },
    en: {
    label: 'Meeting note',
    initial: 'Agenda: report 12 and project 3.',
    saving: 'Saving…',
    saved: (time: string) => `Saved · ${time}`,
    failed: 'Not saved',
    failedHint: 'Your text is still here. Try again when the connection is back.',
    retry: 'Try again',
    },
    };
    type Status = 'idle' | 'saving' | 'saved' | 'failed';
    /** The save starts this long after the user stops typing. */
    export const DEBOUNCE = 1000;
    const clock = (date: Date) => date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
    /**
    * Autosave: typing restarts a 1000 ms debounce; the status line shows progress and the time of the last save.
    * A failed save keeps the text and shows a warning `InlineAlert` with a retry action; a successful save does not toast. `failFirst` makes the first save fail.
    */
    export function Autosave({ lang = 'tr', failFirst = false, delay = 600 }: { lang?: Lang; failFirst?: boolean; delay?: number }) {
    const t = copy[lang];
    const [text, setText] = useState(t.initial);
    const [status, setStatus] = useState<Status>('idle');
    const [savedAt, setSavedAt] = useState('');
    const latestRef = useRef(text);
    const attemptsRef = useRef(0);
    const debounceRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
    const workRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
    const dirtyRef = useRef(false);
    useEffect(
    () => () => {
    clearTimeout(debounceRef.current);
    clearTimeout(workRef.current);
    },
    [],
    );
    useEffect(() => {
    const guard = (event: BeforeUnloadEvent) => {
    if (dirtyRef.current) event.preventDefault();
    };
    window.addEventListener('beforeunload', guard);
    return () => window.removeEventListener('beforeunload', guard);
    }, []);
    const save = (snapshot: string) => {
    setStatus('saving');
    const attempt = attemptsRef.current++;
    workRef.current = setTimeout(() => {
    // Text typed meanwhile starts its own debounce; this result is stale.
    if (latestRef.current !== snapshot) return;
    if (failFirst && attempt === 0) {
    setStatus('failed');
    return;
    }
    dirtyRef.current = false;
    setSavedAt(clock(new Date()));
    setStatus('saved');
    }, delay);
    };
    const onChange = (next: string) => {
    setText(next);
    latestRef.current = next;
    dirtyRef.current = true;
    clearTimeout(debounceRef.current);
    clearTimeout(workRef.current);
    setStatus('idle');
    debounceRef.current = setTimeout(() => save(next), DEBOUNCE);
    };
    return (
    <LangProvider lang={lang}>
    <Stack gap="sm" className="w-full max-w-md">
    <Textarea label={t.label} rows={4} value={text} onValueChange={onChange} />
    <div role="status" className="min-h-6">
    {status === 'saving' ? (
    <Inline gap="xs" align="center">
    <Spinner size="xs" decorative />
    <Text size="sm" tone="subtle">
    {t.saving}
    </Text>
    </Inline>
    ) : status === 'saved' ? (
    <Text size="sm" tone="success">
    {t.saved(savedAt)}
    </Text>
    ) : null}
    </div>
    {status === 'failed' ? (
    <InlineAlert
    tone="warning"
    title={t.failed}
    announce
    actions={
    <Button variant="secondary" size="sm" onClick={() => save(latestRef.current)}>
    {t.retry}
    </Button>
    }
    >
    {t.failedHint}
    </InlineAlert>
    ) : null}
    </Stack>
    </LangProvider>
    );
    }
    Kural Değer
    Açık kaydetme Doğrulaması veya yan etkisi olan formlar için.
    Otomatik kaydetme Belge ve notlar için; yazma bittikten 1000 ms sonra başlar.
    Düğme Yüklenirken loading, bitince success; başarı işareti 2000 ms kalır.
    Sonuç Açık kaydetmede başarı toast’ı: “Değişiklikler kaydedildi”. Otomatik kaydetmede sessiz durum satırı role="status": “Kaydedildi · 10:00”; her kayıtta toast çıkmaz.
    Hata Girilen veri asla silinmez. Açık kaydetmede tehlike düzeyinde InlineAlert, otomatik kaydetmede uyarı düzeyinde InlineAlert; ikisinde de “Tekrar dene” eylemi vardır.
    Ayrılma koruması Kaydedilmemiş veri varken tarayıcıda beforeunload; uygulama içinde onay iletişim kutusu.
    Çifte duyuru Düğmenin kendi başarı duyurusu kapalıdır (successLabel=""); sonucu toast duyurur.
    YapınHata, verinin korunduğunu söyler ve yeniden deneme sunar.
    YapmayınHata sonrası alanı temizlemek kullanıcının emeğini siler.
    1. Açık kaydetmede kullanıcı alanları değiştirir ve “Kaydet” düğmesine basar.
    2. Düğme yükleniyor durumuna geçer ve bir kez “Kaydediliyor” duyurulur.
    3. Başarıda düğme başarı işaretini 2000 ms gösterir; toast “Değişiklikler kaydedildi” der. Düğme ayrıca duyuru yapmaz.
    4. Başarısızlıkta alan değerleri durur. Tehlike düzeyindeki InlineAlert ne olduğunu ve ne yapılacağını söyler; “Tekrar dene” eylemi aynı kaydı yeniden gönderir.
    5. Otomatik kaydetmede her tuş vuruşu 1000 ms’lik bekleme süresini sıfırlar. Süre dolunca “Kaydediliyor…”, ardından “Kaydedildi · 10:00” görünür. Kayıt başarısız olursa uyarı düzeyinde InlineAlert “Tekrar dene” eylemiyle çıkar.
    6. Kayıt sürerken yazılan metin yeni bir bekleme başlatır; eski kaydın sonucu o metni geçersiz kılmaz.

    Doğrulama hataları için Formlar ve doğrulama kalıbına bakın.

    • Açık kaydetmede başarı toast’ı nazik duyurulur; otomatik kaydetmede ilerleme ve sonuç role="status" bölgesinde nazik duyurulur. Yazarken kesinti olmaz.
    • Başarısız açık kayıt role="alert" ile duyurulur, çünkü kullanıcının harekete geçmesi gerekir. Başarısız otomatik kayıt uyarı düzeyindedir ve announce ile nazik duyurulur.
    • Başarı işareti tek başına bilgi taşımaz; toast metni her zaman vardır.
    • “Tekrar dene” düğmesi klavyeyle erişilebilir ve metin kutusunun hemen altındadır.
    • Kaydedilmemiş veriyle sayfadan çıkış denemesi tarayıcı onayına bağlanır.
    • Sonucu söyleyin: “Değişiklikler kaydedildi”, “Kaydedilemedi”.
    • Hatada verinin korunduğunu yazın: “Girdiğiniz bilgiler korundu.”
    • Saat 24 saatlik ve kısa yazılır: “Kaydedildi · 10:00”.
    • Düğme fiil ile başlar ve noktasızdır: “Kaydet”, “Tekrar dene”.
    Öğe Hareket Preset
    Kaydet düğmesi Basınca 0,97 ölçek; 70 ms aşağı, 230 ms geri motion.press
    Başarı işareti 0,6 ile 1 arası 240 ms, 2000 ms görünür kalır motion.success
    Toast ve durum satırı Toast girişte 240 ms; durum satırı 110 ms solma motion.toast, motion.reveal

    Hareketi azaltma tercihinde ölçek kalkar; başarı işareti durağan olarak görünür.

    Button, InlineAlert, Toast, TextInput, Textarea, Spinner. Sonraki kalıplar: Yükleme durumları ve Hatadan kurtarma.

    Uygulama içinde kaydedilmemiş değişiklikle ayrılma onayı için AlertDialog kullanın; ayrıntı: Onay.