Selasa, 26 Agustus 2025

Aplikasi Jastip Warung Teh Ulpa (Flutter Web)


Aplikasi Jastip Warung Teh Ulpa (Flutter Web)

Demo: https://zwbm067uwbn0.zapp.page/#/

Proyek ini membantu siswa/siswi SMKN 1 Banjar memesan makanan di kantin Warung Teh Ulpa via aplikasi web. Fitur utama: katalog menu, keranjang, catatan per item, checkout (wajib isi Nama/Kelas dan No. Telepon), serta struk belanja untuk ditunjukkan ke jastip/tukang.

Stack singkat: Flutter Web • Provider • Intl (locale id_ID) • Aset gambar lokal (statis) • Pembayaran COD (tip dibayar langsung, tidak dihitung aplikasi).

Daftar Isi

  1. Tampilan Awal (Katalog)
  2. Menu Checkout
  3. Struk Belanja
  4. Potongan Kode Penting
  5. Penutup

1) Tampilan Awal (Katalog)

Pada halaman awal, pengguna melihat daftar produk dengan gambar, judul, subjudul, harga, serta kontrol tambah/kurang. Produk tertentu bergabung sebagai varian (misalnya Bala-bala / Karoket dan Gurandil / Rencang) dengan tombol pilihan varian.


Gambar 1. Katalog produk Warung Teh Ulpa (klik gambar produk untuk preview besar).

2) Menu Checkout

Setelah memilih produk, pengguna masuk ke halaman checkout. Di sini tampil ringkasan pesanan, tombol edit catatan per item, dan form wajib Nama/Kelas serta Nomor Telepon. Metode pembayaran hanya COD.


Gambar 2. Checkout dengan ringkasan belanja & form data pembeli.

3) Struk Belanja

Setelah menekan tombol Buat Struk, aplikasi menampilkan struk bergaya minimarket yang memuat Order ID, Tanggal, Nama/Kelas, No. Telepon, serta daftar item dan total. Struk ini cukup ditunjukkan ke jastip/tukang sebagai daftar belanja.


Gambar 3. Struk pesanan siap ditunjukkan ke jastip.

4) Potongan Kode Penting

A. Inisialisasi Locale (hindari LocaleDataException)

// main.dart (bagian atas)
import 'package:intl/intl.dart';
import 'package:intl/date_symbol_data_local.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await initializeDateFormatting('id_ID', null); // penting untuk Flutter Web
  Intl.defaultLocale = 'id_ID';
  runApp(const JastipApp());
}

B. Data Produk (termasuk varian gabungan)

// CatalogProvider: contoh produk + varian
final List<Product> _items = const [
  Product(
    id: 'gehu',
    name: 'Gehu',
    price: 1000,
    assetPath: 'assets/images/gehu.jpg',
    subtitle: 'Tahu isi, cocok pakai cabe',
  ),
  // Bala-bala / Karoket (1 gambar, 2 varian harga sama)
  Product(
    id: 'balakaroket',
    name: 'Bala-bala / Karoket',
    assetPath: 'assets/images/karoket.jpg',
    subtitle: 'Pilih salah satu',
    variants: [
      ProductVariant(key: 'balabala', name: 'Bala-bala', price: 1000),
      ProductVariant(key: 'karoket', name: 'Karoket', price: 1000),
    ],
  ),
  // Gurandil / Rencang (harga berbeda)
  Product(
    id: 'guranren',
    name: 'Gurandil / Rencang',
    assetPath: 'assets/images/rencang_gurandil.jpg',
    subtitle: 'Pilih salah satu',
    variants: [
      ProductVariant(key: 'gurandil', name: 'Gurandil', price: 3000),
      ProductVariant(key: 'rencang', name: 'Rencang', price: 3500),
    ],
  ),
];

C. Form Checkout (wajib Nama/Kelas & Nomor Telepon)

// Di CheckoutPage: form validasi data pembeli
final _formKey = GlobalKey<FormState>();
final _buyerClassCtrl = TextEditingController();
final _buyerPhoneCtrl = TextEditingController();

Form(
  key: _formKey,
  child: Card(
    child: Padding(
      padding: const EdgeInsets.all(12),
      child: Column(
        children: [
          TextFormField(
            controller: _buyerClassCtrl,
            decoration: const InputDecoration(
              labelText: 'Nama/Kelas',
              hintText: 'Contoh: Andi • XI RPL 2',
            ),
            validator: (v) {
              if (v == null || v.trim().isEmpty) return 'Nama/Kelas wajib diisi';
              if (v.length < 3) return 'Terlalu pendek';
              return null;
            },
          ),
          SizedBox(height: 12),
          TextFormField(
            controller: _buyerPhoneCtrl,
            keyboardType: TextInputType.phone,
            decoration: const InputDecoration(
              labelText: 'Nomor Telepon',
              hintText: 'Contoh: 08xxxxxxxxxx',
            ),
            validator: (v) {
              if (v == null || v.trim().isEmpty) return 'Nomor telepon wajib diisi';
              final d = v.replaceAll(RegExp(r'[^0-9]'), '');
              if (d.length < 10 || d.length > 15) return 'Nomor telepon tidak valid';
              return null;
            },
          ),
        ],
      ),
    ),
  ),
);

D. Membuat Struk & Navigasi

// Di tombol "Buat Struk" (CheckoutPage)
onPressed: () {
  if (!_formKey.currentState!.validate()) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Lengkapi data pembeli dulu ya.')),
    );
    return;
  }

  final orderId = generateOrderId();
  final data = OrderTicketData(
    orderId: orderId,
    total: cart.subtotal,
    createdAt: DateTime.now(),
    buyerClass: _buyerClassCtrl.text.trim(),
    buyerPhone: _buyerPhoneCtrl.text.trim(),
    items: cart.items.values.map((e) => TicketItem(
      name: e.displayName, qty: e.qty, price: e.unitPrice, note: e.note,
    )).toList(),
  );

  cart.clear();
  Navigator.pushReplacement(
    context,
    MaterialPageRoute(builder: (_) => OrderReceiptPage(data: data)),
  );
},

E. Tampilan Struk (ringkas)

// OrderReceiptPage: menampilkan detail struk
Column(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    Text('Warung Teh Ulpa', style: TextStyle(fontWeight: FontWeight.w800)),
    Text('Order ID: ${data.orderId}'),
    Text('Tanggal: ${formatDateTime(data.createdAt)}'),
    Text('Nama/Kelas: ${data.buyerClass}'),
    Text('No. Telepon: ${data.buyerPhone}'),
    Divider(),
    ...data.items.map((e) => Row(
      children: [
        Expanded(child: Text('${e.name}  x${e.qty}')),
        Text(rupiah(e.price * e.qty)),
      ],
    )),
    Divider(),
    Row(
      children: [
        Expanded(child: Text('Total Bayar (COD)', style: TextStyle(fontWeight: FontWeight.w800))),
        Text(rupiah(data.total), style: TextStyle(fontWeight: FontWeight.w900)),
      ],
    ),
  ],
)

5) Penutup

Dengan pendekatan data statis + varian per kartu, catatan per item, dan struk bergaya minimarket, alur jastip menjadi jelas: pembeli memilih menu, menambahkan catatan, mengisi data, lalu menunjukkan struk ke jastip/tukang. Pembayaran COD memudahkan di lingkungan sekolah, sementara tip dibayar langsung di tempat.

Tips: Untuk menambah gambar baru, letakkan di assets/images/ dan pastikan path sama dengan di assetPath produk.

Belajar CRUD Flutter + MockAPI.io

 

---


# πŸ“± Belajar CRUD Flutter + MockAPI.io


Halo semua πŸ‘‹

Saya baru saja membuat aplikasi sederhana menggunakan **Flutter** yang terhubung dengan **MockAPI.io**. Aplikasi ini bisa melakukan **CRUD (Create, Read, Delete, dan nanti bisa ditambah Update)** dengan database online.


## πŸ”— Link Project Demo


πŸ‘‰ [Coba aplikasinya di sini](https://zf48062dg490.zapp.page/#/)


## πŸ’» Source Code Lengkap


Kalau mau lihat langsung kode Flutter-nya, bisa cek di Pastebin berikut:

πŸ‘‰ [Lihat source code](https://pastebin.com/Sn1ATvZ4)


## πŸ› ️ Fitur Aplikasi


✨ Input data (Nama, Email, Tempat Lahir, Tanggal Lahir)

✨ Menampilkan data dari API

✨ Hapus data dari daftar

✨ Format tanggal rapi tanpa `T00:00:00.000`


## 🎯 Tujuan Project


Project ini dibuat untuk **belajar koneksi Flutter ke API** dengan cara yang mudah, cepat, dan gratis menggunakan MockAPI.io.


---


Jadi, buat teman-teman yang lagi belajar Flutter πŸš€ silakan coba juga, semoga bermanfaat!

Kalau ada yang mau tanya atau butuh file project, langsung aja komentar ✨


---


Selasa, 12 Agustus 2025

Membuat login mobile menggunakan flutter

πŸš€ My Flutter Login Page — Simple UI, Big Vibes ✨ + Smooth Animation πŸ’»πŸ”₯



Yo fam! πŸ‘‹
Hari ini gue mau flex hasil ngulik di Flutter — yes, a clean & modern mobile login page yang sekarang udah pake smooth animation. πŸ’»✨

Bayangin, pas app dibuka, form login nggak langsung nongol gitu aja, tapi muncul dari bawah dengan animasi halus ala aplikasi premium. 🫢πŸ”₯

Highlight-nya:
πŸ‘€ Big profile icon on top
πŸ”’ Smooth username & password input fields
πŸ‘ Show/hide password button — so you don’t mess up your password
🟠 Bold “Sign in” button
🎬 Slide-up animation pas masuk halaman login


Nah, buat lo yang pengen nyoba bikin login page ini, gue kasih langsung kode lengkapnya. Tinggal copy → paste → run di Flutter. πŸš€

import 'package:flutter/material.dart';

void main() {
  runApp(const MaterialApp(
    debugShowCheckedModeBanner: false,
    home: LoginPage(),
  ));
}

class LoginPage extends StatefulWidget {
  const LoginPage({Key? key}) : super(key: key);

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage>
    with SingleTickerProviderStateMixin {
  bool _obscurePassword = true;
  late AnimationController _controller;
  late Animation<Offset> _slideAnimation;

  @override
  void initState() {
    super.initState();

    _controller = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 700),
    );

    _slideAnimation = Tween<Offset>(
      begin: const Offset(0, 1), // mulai dari bawah
      end: Offset.zero, // posisi normal
    ).animate(CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOutCubic,
    ));

    // mulai animasi saat halaman muncul
    _controller.forward();
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: const Color(0xFF009688),
      body: Center(
        child: SingleChildScrollView(
          child: Column(
            children: [
              const Icon(Icons.account_circle, size: 100, color: Colors.white),
              const SizedBox(height: 20),
              SlideTransition(
                position: _slideAnimation,
                child: Container(
                  margin: const EdgeInsets.symmetric(horizontal: 20),
                  padding: const EdgeInsets.all(20),
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(12),
                    boxShadow: [
                      BoxShadow(
                        color: Colors.black26,
                        blurRadius: 6,
                        offset: const Offset(0, 2),
                      ),
                    ],
                  ),
                  child: Column(
                    children: [
                      TextField(
                        decoration: InputDecoration(
                          prefixIcon: const Icon(Icons.person),
                          labelText: 'User Name',
                          border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(8),
                          ),
                        ),
                      ),
                      const SizedBox(height: 15),
                      TextField(
                        obscureText: _obscurePassword,
                        decoration: InputDecoration(
                          prefixIcon: const Icon(Icons.lock),
                          suffixIcon: IconButton(
                            icon: Icon(
                              _obscurePassword
                                  ? Icons.visibility_off
                                  : Icons.visibility,
                            ),
                            onPressed: () {
                              setState(() {
                                _obscurePassword = !_obscurePassword;
                              });
                            },
                          ),
                          labelText: 'Password',
                          border: OutlineInputBorder(
                            borderRadius: BorderRadius.circular(8),
                          ),
                        ),
                      ),
                      const SizedBox(height: 10),
                      Align(
                        alignment: Alignment.centerRight,
                        child: TextButton(
                          onPressed: () {},
                          child: const Text('Forgot Password?'),
                        ),
                      ),
                      const SizedBox(height: 10),
                      SizedBox(
                        width: double.infinity,
                        height: 45,
                        child: ElevatedButton(
                          style: ElevatedButton.styleFrom(
                            backgroundColor: Colors.orange,
                            shape: RoundedRectangleBorder(
                              borderRadius: BorderRadius.circular(8),
                            ),
                          ),
                          onPressed: () {},
                          child: const Text(
                            'Sign in',
                            style: TextStyle(fontSize: 16),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Penutup:

That’s it gengs! 🎯
Sekarang lo udah punya login page Flutter yang bukan cuma simple & modern, tapi juga punya smooth animation biar keliatan premium. πŸš€✨

Kalau lo baru belajar Flutter, project ini cocok banget buat latihan karena:

  • Lo bakal ngerti dasar layout widget

  • Lo belajar handle input text & tombol

  • Lo bisa explore animasi biar UI lebih hidup

Next level? Tinggal sambungin ke Firebase Auth atau API backend biar login ini beneran berfungsi. πŸ”₯

So… gimana?
Apakah login page ini udah pass the vibe check menurut lo? 😏
Drop pendapat lo di kolom komentar, dan jangan lupa share ke temen coding lo biar makin rame!

Happy coding & keep building awesome stuff! πŸ’»πŸ’™

#FlutterDev #UIUX #GenZCoding #CodeLife #MobileDev



Selasa, 05 Agustus 2025

Bikin Fitur Maps di ZaapRun Flutter, Ada Tombol Rute & WhatsApp Gesss πŸ—Ί️πŸ’¬






Heyy sobat ngoding! 😎
Di project Flutter gue yang bernama ZaapRun, gue lagi nambahin fitur yang menurut gue wajib banget ada di aplikasi kekinian — yaitu Maps pake iframe, lengkap sama tombol rute dan tombol chat ke WA! πŸ”₯

🧠 Kenapa Nggak Pake Plugin Maps?

Gue tau sih di Flutter ada plugin kayak google_maps_flutter, tapi ribet bro, harus API key, setting sana-sini, kadang error juga. Jadi gue cari cara yang lebih cepet dan simpel: langsung sematkan Google Maps pake iframe di dalam WebView.
Lebih ringan, lebih clean, dan tetep works!


πŸ”§ Cara Gue Nerapin di Flutter

Gue pake package webview_flutter buat nampilin HTML-nya. Nah di HTML itu gue tempelin iframe maps + 2 tombol penting:

WebView(
  initialUrl: Uri.dataFromString(
    '''
    <html>
    <body style="margin:0;padding:0;text-align:center;font-family:sans-serif;">
      <iframe 
        src="https://www.google.com/maps/embed?pb=..." 
        width="100%" height="350" style="border:0;" allowfullscreen loading="lazy">
      </iframe>
      <br><br>
      <a href="https://www.google.com/maps/dir/?api=1&destination=-6.123456,106.123456" target="_blank">
        <button style="padding:10px 25px;border:none;background:#000;color:#fff;border-radius:8px;">πŸ“ Buka Rute</button>
      </a>
      <br><br>
      <a href="https://wa.me/6281234567890" target="_blank">
        <button style="padding:10px 25px;border:none;background:#25D366;color:#fff;border-radius:8px;">πŸ’¬ Chat WA</button>
      </a>
    </body>
    </html>
    ''',
    mimeType: 'text/html'
  ).toString(),
  javascriptMode: JavascriptMode.unrestricted,
)

πŸš€ Hasilnya Gimana?

  • Bisa liat lokasi langsung di aplikasi.

  • Bisa klik tombol "Buka Rute" buat langsung navigasi ke lokasi via Google Maps.

  • Bisa klik tombol "Chat WA" buat langsung ngobrol.

Semua langsung jalan, tanpa perlu install plugin ribet, tanpa loading berat. Simpel, clean, dan user-friendly banget.


πŸ’­ Buat Apa Sih Fitur Ini?

Cocok buat lo yang mau bikin aplikasi:

  • Lokasi bisnis πŸͺ

  • Titik jemput/antar πŸ›΅

  • Map sekolah, rumah, kampus, atau event πŸ“

  • Dan pokoknya biar user langsung tau: "Gue harus ke mana nih?"


Segitu dulu cerita gue kali ini.
Kalo lo pengen nyoba juga, tinggal copas aja konsep ini. Dijamin gampang dan cepet jadi. 😎
Mau tanya-tanya? Klik tombol WhatsApp aja gengs! πŸ’¬

Peace out, stay ngoding! ✌️



WARUNG GORENGAN - SENDI

🍽️ Warung Gorengan Aplikasi Jastip Gorengan Kantin - Solusi Modern untuk Pesan...