Preskočiť na obsah

Ako vytvoriť dopytový formulár vo Flutteri

Dopytový formulár vo Flutteri s výberom služby, rozpočtu a termínu, ktorý vám každý dopyt pošle e-mailom a zákazníkovi automaticky potvrdí jeho prijatie – bez backendu.

Aktualizované:

Video je v angličtine; nižšie nájdete celý návod v slovenčine.

V tomto návode vytvoríte vo Flutteri obrazovku na dopyt: zákazník vyberie službu, rozpočet a termín, popíše projekt a ťukne na Send. Vy dostanete prehľadne naformátovaný e-mail, zákazník automatické potvrdenie.

Potrebujete: bezplatný účet Frontmail, projekt vo Flutteri 3.24+ a zhruba dvadsať minút.

1. Pripojte schránku a vytvorte šablóny

  1. E-mailové služby → Pridať službu: pripojte svoju schránku alebo poskytovateľa, zaškrtnite Nastaviť ako predvolenú, Overiť a uložiť.
  2. Šablóny → Z galérie → Dopyt (kategória Biznis) → Použiť šablónu.
  3. Pridajte potvrdenie: Z galérie → Automatická odpoveď: ďakujeme za správu → Použiť šablónu, na jej karte Parametre nastavte predvolené hodnoty pre company_name a response_time a potom ju vyberte v šablóne dopytu v Nastavenia → Doručovanie → Šablóna automatickej odpovede.

Šablóna dopytu očakáva name, email, service a details (povinné) a phone, company, budget, deadline (nepovinné). service a budget prijímajú iba hodnoty uvedené na karte Parametre. Príklad kódu používa hodnoty anglickej šablóny; keď šablónu vytvoríte v slovenčine, hodnoty budú slovenské (Web, E-shop, Mobilná aplikácia, Konzultácie, Iné a Do 5 000 €, 5 000 – 20 000 €, 20 000 – 50 000 €, Nad 50 000 €). Upravte zoznamy podľa toho, čo ponúkate, a v aplikácii posielajte presne tie isté reťazce.

2. Povoľte mobilné aplikácie a zapnite CAPTCHA

  • Zabezpečenie → Mobilné aplikácie → Povoliť mobilné aplikácie → Uložiť. Bez toho sa požiadavky bez webovej hlavičky Origin odmietnu s chybou origin_not_allowed.
  • Šablóna dopytu → Nastavenia → Ochrana → Vyžadovať CAPTCHA (Turnstile). Aplikácie používajú zdieľaný widget Frontmailu – účet v Cloudflare nepotrebujete.

3. Inštalácia

flutter pub add frontmail webview_flutter url_launcher

Z časti o Turnstile v dokumentácii Flutter SDK si skopírujte TurnstileView do lib/turnstile_view.dart.

4. Vytvorte formulár

import 'package:flutter/material.dart';
import 'package:frontmail/frontmail.dart';

import 'turnstile_view.dart';

final frontmail = Frontmail(publicKey: 'pk_…');

const services = ['Website', 'E-shop', 'Mobile app', 'Consulting', 'Other'];
const budgets = ['Under \$5k', '\$5k–20k', '\$20k–50k', 'Over \$50k'];

class QuoteScreen extends StatefulWidget {
  const QuoteScreen({super.key});

  @override
  State<QuoteScreen> createState() => _QuoteScreenState();
}

class _QuoteScreenState extends State<QuoteScreen> {
  final _name = TextEditingController();
  final _email = TextEditingController();
  final _details = TextEditingController();
  String _service = services.first;
  String? _budget;
  DateTime? _deadline;
  TurnstileSite? _site;
  String? _token;
  int _resetId = 0;
  String _info = '';

  @override
  void initState() {
    super.initState();
    Turnstile.site(client: frontmail).then((site) => setState(() => _site = site));
  }

  Future<void> _pickDeadline() async {
    final now = DateTime.now();
    final picked = await showDatePicker(context: context, firstDate: now, lastDate: DateTime(now.year + 2));
    if (picked != null) setState(() => _deadline = picked);
  }

  Future<void> _send() async {
    final site = _site, token = _token;
    if (site == null || token == null) return;
    setState(() {
      _token = null; // a token works only once
      _resetId++;
    });
    try {
      final deadline = _deadline;
      await frontmail.send(
        templateId: 'tpl_…',
        params: {
          'name': _name.text,
          'email': _email.text,
          'service': _service,
          'budget': _budget,
          'deadline': deadline == null ? null : deadline.toIso8601String().substring(0, 10),
          'details': _details.text,
        },
        options: SendOptions(turnstileToken: token, turnstileKey: site.turnstileKey),
      );
      setState(() => _info = 'Thanks! We’ve emailed you a confirmation.');
    } on FrontmailException catch (e) {
      setState(() => _info = e.message);
    }
  }

  @override
  Widget build(BuildContext context) {
    final site = _site;
    return Scaffold(
      appBar: AppBar(title: const Text('Request a quote')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          TextField(controller: _name, decoration: const InputDecoration(labelText: 'Name')),
          TextField(controller: _email, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress),
          DropdownButtonFormField<String>(
            value: _service,
            decoration: const InputDecoration(labelText: 'What do you need?'),
            items: [for (final s in services) DropdownMenuItem(value: s, child: Text(s))],
            onChanged: (v) => setState(() => _service = v ?? services.first),
          ),
          DropdownButtonFormField<String>(
            value: _budget,
            decoration: const InputDecoration(labelText: 'Budget (optional)'),
            items: [for (final b in budgets) DropdownMenuItem(value: b, child: Text(b))],
            onChanged: (v) => setState(() => _budget = v),
          ),
          ListTile(
            contentPadding: EdgeInsets.zero,
            title: Text(_deadline == null ? 'Deadline (optional)' : 'Deadline: ${MaterialLocalizations.of(context).formatMediumDate(_deadline!)}'),
            trailing: const Icon(Icons.calendar_today),
            onTap: _pickDeadline,
          ),
          TextField(controller: _details, decoration: const InputDecoration(labelText: 'Project details'), maxLines: 5),
          const SizedBox(height: 16),
          if (site != null)
            TurnstileView(
              site: site,
              resetId: _resetId,
              onMessage: (m) => setState(() => _token = m is TurnstileToken ? m.token : null),
            ),
          FilledButton(onPressed: _token == null ? null : _send, child: const Text('Request a quote')),
          Text(_info),
        ],
      ),
    );
  }
}

Hodnoty null (bez rozpočtu, bez termínu) sa považujú za prázdne, takže sa tieto riadky v e-maili jednoducho nezobrazia. Termín sa posiela ako YYYY-MM-DD; e-mail ho zobrazí ako naformátovaný dátum.

5. Vyskúšajte to

Odošlite dopyt s vlastnou adresou ako e-mailom zákazníka. Dostanete dopyt a hneď po ňom aj potvrdenie. Oba e-maily nájdete v História.

Každý e-mail stojí jeden kredit – dopyt aj s potvrdením stojí dva. Došli vám kredity v rušnom týždni? Dopyty počkajú vo fronte pozdržaných e-mailov a odídu po dobití; stav výsledku je held.

Ďalšie kroky

  • V nastaveniach šablóny zapnite Zber kontaktov a budete mať zoznam všetkých, ktorí žiadali o ponuku (email, name a company sú už namapované).
  • Pošlite SendOptions(locale: Localizations.localeOf(context).toLanguageTag()) a použije sa jazyková verzia šablóny pre jazyk používateľa.
Všetky návody
  • Video, 42 s

    FlutterKontaktný formulár

    Ako vytvoriť kontaktný formulár vo Flutteri, ktorý posiela e-maily

    Kontaktný formulár vo Flutteri, ktorý vám každú správu pošle e-mailom – bez backendu a bez SMTP hesla v aplikácii. Funguje na Androide, iOS, webe aj desktope s balíčkom Frontmail pre Dart.

  • Video, 42 s

    WebDopytový formulár

    Ako vytvoriť dopytový formulár na web

    Dopytový formulár so službou, rozpočtom a termínom, ktorý vám príde ako prehľadný e-mail – a zákazníkovi automaticky pošle potvrdenie. Bez backendu, v čistom HTML.

  • Video, 42 s

    React NativeDopytový formulár

    Ako vytvoriť dopytový formulár v React Native

    Dopytový formulár v React Native a Expo s výberom služby, rozpočtu a termínu, ktorý vám každý dopyt pošle e-mailom a zákazníkovi ho automaticky potvrdí.