Belajar membuat website whatsapp sender

 🔥 Membuat Website WhatsApp Sender Pro dengan Flutter (DartPad Compatible)

Halo teman-teman! 👋
Kali ini gue mau share project keren yang bisa kalian coba langsung di browser tanpa install apa-apa — yaitu WhatsApp Sender berbasis Flutter Web 🚀

Project ini memungkinkan kalian untuk:

  • 📩 Kirim pesan WhatsApp tanpa simpan nomor

  • 🌍 Pilih & cari kode negara (lengkap + search!)

  • 🇮🇩 Dilengkapi flag tiap negara

  • ⭐ Simpan nomor favorit

  • 📜 Menyimpan riwayat pesan

  • 🌙 Toggle dark mode

  • 💾 Data tersimpan otomatis di browser (localStorage)


🖥️ Tampilan Aplikasi

Aplikasi ini punya UI modern dengan:

  • Warna dominan biru cerah 💙

  • Card design clean & minimalis

  • Navigasi bawah (Kirim, Favorit, Riwayat)

  • Responsive untuk web




⚙️ Cara Kerja

Aplikasi ini menggunakan link resmi WhatsApp:

https://wa.me/nomor?text=pesan

Jadi saat klik tombol kirim:
➡️ Otomatis membuka WhatsApp Web
➡️ Pesan langsung terisi
➡️ Tinggal tekan kirim di WhatsApp


🧠 Fitur Utama

🔍 1. Search Negara

  • Bisa cari berdasarkan nama negara / kode

  • UX mirip aplikasi profesional

⭐ 2. Favorit

  • Simpan nomor penting

  • Bisa klik langsung untuk isi otomatis

📜 3. History Pesan

  • Menyimpan riwayat pengiriman

  • Ada timestamp

  • Bisa dibersihkan kapan saja

🎯 4. Smart Input

  • Auto format nomor (hapus simbol aneh)

  • Validasi input sebelum kirim


💡 Kelebihan Project Ini

  • ❌ Tidak perlu backend

  • ❌ Tidak perlu database

  • ✅ Bisa jalan di DartPad

  • ✅ Ringan & cepat

  • ✅ Cocok buat portfolio anak RPL 😎


🚀 Cara Menjalankan

  1. Buka DartPad (Flutter mode)

  2. Copy paste kode ini: 

    import 'dart:convert';

    import 'dart:html' as html;


    import 'package:flutter/material.dart';

    import 'package:flutter/foundation.dart'; // Import for kIsWeb


    void main() {

      runApp(const MyApp());

    }


    class MyApp extends StatelessWidget {

      const MyApp({super.key});


      @override

      Widget build(BuildContext context) {

        return MaterialApp(

          debugShowCheckedModeBanner: false,

          title: 'WhatsApp Sender Pro',

          theme: ThemeData(

            colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),

            useMaterial3: true,

          ),

          home: const HomePage(),

        );

      }

    }


    class CountryItem {

      final String code;

      final String name;

      final String flag;


      const CountryItem({

        required this.code,

        required this.name,

        required this.flag,

      });


      Map<String, dynamic> toJson() => {

            'code': code,

            'name': name,

            'flag': flag,

          };


      factory CountryItem.fromJson(Map<String, dynamic> json) => CountryItem(

            code: json['code'] as String,

            name: json['name'] as String,

            flag: json['flag'] as String,

          );

    }


    class FavoriteContact {

      final String phone;

      final String countryCode;

      final String countryName;

      final String flag;

      final String label;


      const FavoriteContact({

        required this.phone,

        required this.countryCode,

        required this.countryName,

        required this.flag,

        required this.label,

      });


      Map<String, dynamic> toJson() => {

            'phone': phone,

            'countryCode': countryCode,

            'countryName': countryName,

            'flag': flag,

            'label': label,

          };


      factory FavoriteContact.fromJson(Map<String, dynamic> json) => FavoriteContact(

            phone: json['phone'] as String,

            countryCode: json['countryCode'] as String,

            countryName: json['countryName'] as String,

            flag: json['flag'] as String,

            label: json['label'] as String,

          );

    }


    class MessageHistoryItem {

      final String phone;

      final String countryCode;

      final String countryName;

      final String flag;

      final String message;

      final DateTime time;


      const MessageHistoryItem({

        required this.phone,

        required this.countryCode,

        required this.countryName,

        required this.flag,

        required this.message,

        required this.time,

      });


      Map<String, dynamic> toJson() => {

            'phone': phone,

            'countryCode': countryCode,

            'countryName': countryName,

            'flag': flag,

            'message': message,

            'time': time.toIso8601String(),

          };


      factory MessageHistoryItem.fromJson(Map<String, dynamic> json) => MessageHistoryItem(

            phone: json['phone'] as String,

            countryCode: json['countryCode'] as String,

            countryName: json['countryName'] as String,

            flag: json['flag'] as String,

            message: json['message'] as String,

            time: DateTime.tryParse(json['time'] as String? ?? '') ?? DateTime.now(),

          );

    }


    class HomePage extends StatefulWidget {

      const HomePage({super.key});


      @override

      State<HomePage> createState() => _HomePageState();

    }


    class _HomePageState extends State<HomePage> {

      static const String favoritesKey = 'wa_favorites_v1';

      static const String historyKey = 'wa_history_v1';


      final TextEditingController phoneController = TextEditingController();

      final TextEditingController messageController = TextEditingController();

      final TextEditingController countrySearchController = TextEditingController();

      final TextEditingController favoriteLabelController = TextEditingController();


      bool isDarkMode = false;

      int selectedTab = 0;

      bool useBoldMessagePreview = false;


      late List<CountryItem> countryCodes;

      late CountryItem selectedCountry;


      List<FavoriteContact> favorites = [];

      List<MessageHistoryItem> history = [];


      @override

      void initState() {

        super.initState();

        countryCodes = _buildCountries();

        selectedCountry = countryCodes.firstWhere((c) => c.code == '+62', orElse: () => countryCodes.first);

        // Load saved data; helper methods will handle web-only context and potential errors.

        _loadSavedData();

      }


      @override

      void dispose() {

        phoneController.dispose();

        messageController.dispose();

        countrySearchController.dispose();

        favoriteLabelController.dispose();

        super.dispose();

      }


      List<CountryItem> _buildCountries() {

        return const [

          CountryItem(code: '+62', name: 'Indonesia', flag: '🇮🇩'),

          CountryItem(code: '+1', name: 'United States / Canada', flag: '🇺🇸'),

          CountryItem(code: '+44', name: 'United Kingdom', flag: '🇬🇧'),

          CountryItem(code: '+91', name: 'India', flag: '🇮🇳'),

          CountryItem(code: '+81', name: 'Japan', flag: '🇯🇵'),

          CountryItem(code: '+82', name: 'South Korea', flag: '🇰🇷'),

          CountryItem(code: '+86', name: 'China', flag: '🇨🇳'),

          CountryItem(code: '+49', name: 'Germany', flag: '🇩🇪'),

          CountryItem(code: '+33', name: 'France', flag: '🇫🇷'),

          CountryItem(code: '+39', name: 'Italy', flag: '🇮🇹'),

          CountryItem(code: '+34', name: 'Spain', flag: '🇪🇸'),

          CountryItem(code: '+7', name: 'Russia', flag: '🇷🇺'),

          CountryItem(code: '+55', name: 'Brazil', flag: '🇧🇷'),

          CountryItem(code: '+61', name: 'Australia', flag: '🇦🇺'),

          CountryItem(code: '+64', name: 'New Zealand', flag: '🇳🇿'),

          CountryItem(code: '+27', name: 'South Africa', flag: '🇿🇦'),

          CountryItem(code: '+20', name: 'Egypt', flag: '🇪🇬'),

          CountryItem(code: '+90', name: 'Turkey', flag: '🇹🇷'),

          CountryItem(code: '+966', name: 'Saudi Arabia', flag: '🇸🇦'),

          CountryItem(code: '+971', name: 'United Arab Emirates', flag: '🇦🇪'),

          CountryItem(code: '+65', name: 'Singapore', flag: '🇸🇬'),

          CountryItem(code: '+60', name: 'Malaysia', flag: '🇲🇾'),

          CountryItem(code: '+66', name: 'Thailand', flag: '🇹🇭'),

          CountryItem(code: '+63', name: 'Philippines', flag: '🇵🇭'),

          CountryItem(code: '+84', name: 'Vietnam', flag: '🇻🇳'),

          CountryItem(code: '+880', name: 'Bangladesh', flag: '🇧🇩'),

          CountryItem(code: '+92', name: 'Pakistan', flag: '🇵🇰'),

          CountryItem(code: '+234', name: 'Nigeria', flag: '🇳🇬'),

          CountryItem(code: '+254', name: 'Kenya', flag: '🇰🇪'),

          CountryItem(code: '+351', name: 'Portugal', flag: '🇵🇹'),

          CountryItem(code: '+46', name: 'Sweden', flag: '🇸🇪'),

          CountryItem(code: '+47', name: 'Norway', flag: '🇳🇴'),

          CountryItem(code: '+45', name: 'Denmark', flag: '🇩🇰'),

          CountryItem(code: '+41', name: 'Switzerland', flag: '🇨🇭'),

          CountryItem(code: '+31', name: 'Netherlands', flag: '🇳🇱'),

          CountryItem(code: '+32', name: 'Belgium', flag: '🇧🇪'),

          CountryItem(code: '+48', name: 'Poland', flag: '🇵🇱'),

          CountryItem(code: '+420', name: 'Czech Republic', flag: '🇨🇿'),

          CountryItem(code: '+30', name: 'Greece', flag: '🇬🇷'),

          CountryItem(code: '+98', name: 'Iran', flag: '🇮🇷'),

          CountryItem(code: '+972', name: 'Israel', flag: '🇮🇱'),

          CountryItem(code: '+52', name: 'Mexico', flag: '🇲🇽'),

          CountryItem(code: '+54', name: 'Argentina', flag: '🇦🇷'),

          CountryItem(code: '+56', name: 'Chile', flag: '🇨🇱'),

          CountryItem(code: '+57', name: 'Colombia', flag: '🇨🇴'),

          CountryItem(code: '+233', name: 'Ghana', flag: '🇬🇭'),

          CountryItem(code: '+212', name: 'Morocco', flag: '🇲🇦'),

          CountryItem(code: '+358', name: 'Finland', flag: '🇫🇮'),

          CountryItem(code: '+353', name: 'Ireland', flag: '🇮🇪'),

          CountryItem(code: '+43', name: 'Austria', flag: '🇦🇹'),

          CountryItem(code: '+385', name: 'Croatia', flag: '🇭🇷'),

        ];

      }


      // Helper function to safely access localStorage

      String? _getLocalStorageItem(String key) {

        if (!kIsWeb) return null; // localStorage is only available on web

        try {

          return html.window.localStorage[key];

        } on html.DomException catch (e) {

          debugPrint('SecurityError accessing localStorage for key "$key": ${e.message}');

          // This happens in sandboxed iframes or file:// protocol.

          // Treat as if the item doesn't exist.

          return null;

        } catch (e) {

          debugPrint('Error accessing localStorage for key "$key": $e');

          return null;

        }

      }


      // Helper function to safely write to localStorage

      void _setLocalStorageItem(String key, String value) {

        if (!kIsWeb) return; // localStorage is only available on web

        try {

          html.window.localStorage[key] = value;

        } on html.DomException catch (e) {

          debugPrint('SecurityError writing to localStorage for key "$key": ${e.message}');

        } catch (e) {

          debugPrint('Error writing to localStorage for key "$key": $e');

        }

      }


      // Helper function to safely remove from localStorage

      void _removeLocalStorageItem(String key) {

        if (!kIsWeb) return; // localStorage is only available on web

        try {

          html.window.localStorage.remove(key);

        } on html.DomException catch (e) {

          debugPrint('SecurityError removing from localStorage for key "$key": ${e.message}');

        } catch (e) {

          debugPrint('Error removing from localStorage for key "$key": $e');

        }

      }


      void _loadSavedData() {

        // Use the safe helper to get data

        final favRaw = _getLocalStorageItem(favoritesKey);

        final histRaw = _getLocalStorageItem(historyKey);


        if (favRaw != null && favRaw.isNotEmpty) {

          try {

            final decoded = jsonDecode(favRaw) as List<dynamic>;

            favorites = decoded

                .map((e) => FavoriteContact.fromJson(Map<String, dynamic>.from(e as Map)))

                .toList();

          } catch (e) {

            debugPrint('Error decoding favorites from localStorage: $e');

            favorites = []; // Clear on decode error

          }

        } else {

          favorites = [];

        }


        if (histRaw != null && histRaw.isNotEmpty) {

          try {

            final decoded = jsonDecode(histRaw) as List<dynamic>;

            history = decoded

                .map((e) => MessageHistoryItem.fromJson(Map<String, dynamic>.from(e as Map)))

                .toList();

          } catch (e) {

            debugPrint('Error decoding history from localStorage: $e');

            history = []; // Clear on decode error

          }

        } else {

          history = [];

        }

        setState(() {});

      }


      void _saveFavorites() {

        // Use the safe helper to set data

        _setLocalStorageItem(favoritesKey, jsonEncode(favorites.map((e) => e.toJson()).toList()));

      }


      void _saveHistory() {

        // Use the safe helper to set data

        _setLocalStorageItem(historyKey, jsonEncode(history.map((e) => e.toJson()).toList()));

      }


      String _normalizePhone(String input) {

        return input.replaceAll(RegExp(r'[^0-9]'), '');

      }


      String _buildWaUrl({required String phone, required String message}) {

        final fullPhone = '${selectedCountry.code}${_normalizePhone(phone)}';

        return 'https://wa.me/$fullPhone?text=${Uri.encodeComponent(message)}';

      }


      void sendWhatsApp() {

        final phone = phoneController.text.trim();

        final message = messageController.text.trim();


        if (phone.isEmpty || message.isEmpty) {

          ScaffoldMessenger.of(context).showSnackBar(

            const SnackBar(content: Text('Isi nomor dan pesan dulu bro')),

          );

          return;

        }


        final url = _buildWaUrl(phone: phone, message: message);

        html.window.open(url, '_blank');


        history.insert(

          0,

          MessageHistoryItem(

            phone: phone,

            countryCode: selectedCountry.code,

            countryName: selectedCountry.name,

            flag: selectedCountry.flag,

            message: message,

            time: DateTime.now(),

          ),

        );


        if (history.length > 30) {

          history = history.sublist(0, 30);

        }

        _saveHistory(); // Uses the safe helper

        setState(() {});

      }


      void openChatWithoutMessage() {

        final phone = phoneController.text.trim();

        if (phone.isEmpty) {

          ScaffoldMessenger.of(context).showSnackBar(

            const SnackBar(content: Text('Isi nomor dulu bro')),

          );

          return;

        }


        final fullPhone = '${selectedCountry.code}${_normalizePhone(phone)}';

        html.window.open('https://wa.me/$fullPhone', '_blank');

      }


      void openApiLink() {

        final phone = phoneController.text.trim();

        final message = messageController.text.trim();

        if (phone.isEmpty || message.isEmpty) {

          ScaffoldMessenger.of(context).showSnackBar(

            const SnackBar(content: Text('Isi nomor dan pesan dulu bro')),

          );

          return;

        }

        final fullPhone = '${selectedCountry.code}${_normalizePhone(phone)}';

        final url =

            'https://api.whatsapp.com/send?phone=$fullPhone&text=${Uri.encodeComponent(message)}';

        html.window.open(url, '_blank');

      }


      void clearAll() {

        phoneController.clear();

        messageController.clear();

        setState(() {});

      }


      void saveFavorite() {

        final phone = phoneController.text.trim();

        if (phone.isEmpty) {

          ScaffoldMessenger.of(context).showSnackBar(

            const SnackBar(content: Text('Isi nomor dulu bro')),

          );

          return;

        }


        final label = favoriteLabelController.text.trim().isEmpty

            ? 'Favorit ${selectedCountry.flag} ${selectedCountry.name}'

            : favoriteLabelController.text.trim();


        final fav = FavoriteContact(

          phone: _normalizePhone(phone),

          countryCode: selectedCountry.code,

          countryName: selectedCountry.name,

          flag: selectedCountry.flag,

          label: label,

        );


        final exists = favorites.any((f) =>

            f.phone == fav.phone && f.countryCode == fav.countryCode);

        if (!exists) {

          favorites.insert(0, fav);

          if (favorites.length > 20) {

            favorites = favorites.sublist(0, 20);

          }

          _saveFavorites(); // Uses the safe helper

          setState(() {});

          ScaffoldMessenger.of(context).showSnackBar(

            const SnackBar(content: Text('Nomor favorit disimpan')),

          );

        } else {

          ScaffoldMessenger.of(context).showSnackBar(

            const SnackBar(content: Text('Nomor favorit sudah ada')),

          );

        }

      }


      void loadFavorite(FavoriteContact fav) {

        setState(() {

          selectedCountry = countryCodes.firstWhere(

            (c) => c.code == fav.countryCode,

            orElse: () => selectedCountry,

          );

          phoneController.text = fav.phone;

          selectedTab = 0;

        });

      }


      void deleteFavorite(FavoriteContact fav) {

        favorites.removeWhere((f) => f.phone == fav.phone && f.countryCode == fav.countryCode);

        _saveFavorites(); // Uses the safe helper

        setState(() {});

      }


      void clearHistory() {

        history.clear();

        _saveHistory(); // Uses the safe helper

        setState(() {});

      }


      Future<void> openCountryPicker() async {

        final chosen = await showModalBottomSheet<CountryItem>(

          context: context,

          isScrollControlled: true,

          showDragHandle: true,

          shape: const RoundedRectangleBorder(

            borderRadius: BorderRadius.vertical(top: Radius.circular(24)),

          ),

          builder: (context) {

            List<CountryItem> filtered = List.from(countryCodes);


            return StatefulBuilder(

              builder: (context, setModalState) {

                final query = countrySearchController.text.trim().toLowerCase();

                filtered = query.isEmpty

                    ? countryCodes

                    : countryCodes.where((c) {

                        return c.name.toLowerCase().contains(query) ||

                            c.code.contains(query) ||

                            c.flag.contains(query);

                      }).toList();


                return Padding(

                  padding: EdgeInsets.only(

                    left: 16,

                    right: 16,

                    top: 8,

                    bottom: MediaQuery.of(context).viewInsets.bottom + 16,

                  ),

                  child: Column(

                    mainAxisSize: MainAxisSize.min,

                    children: [

                      const Text(

                        'Pilih Negara',

                        style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),

                      ),

                      const SizedBox(height: 12),

                      TextField(

                        controller: countrySearchController,

                        onChanged: (_) => setModalState(() {}),

                        decoration: InputDecoration(

                          prefixIcon: const Icon(Icons.search),

                          hintText: 'Cari negara atau kode',

                          border: OutlineInputBorder(

                            borderRadius: BorderRadius.circular(14),

                          ),

                        ),

                      ),

                      const SizedBox(height: 12),

                      SizedBox(

                        height: 420,

                        child: ListView.separated(

                          itemCount: filtered.length,

                          separatorBuilder: (_, __) => const Divider(height: 1),

                          itemBuilder: (_, index) {

                            final c = filtered[index];

                            return ListTile(

                              leading: CircleAvatar(

                                child: Text(c.flag),

                              ),

                              title: Text(c.name),

                              subtitle: Text(c.code),

                              onTap: () => Navigator.pop(context, c),

                            );

                          },

                        ),

                      ),

                    ],

                  ),

                );

              },

            );

          },

        );


        countrySearchController.clear();

        if (chosen != null) {

          setState(() {

            selectedCountry = chosen;

          });

        }

      }


      Widget buildDashboard() {

        return SingleChildScrollView(

          padding: const EdgeInsets.all(16),

          child: Column(

            crossAxisAlignment: CrossAxisAlignment.stretch,

            children: [

              _headerCard(),

              const SizedBox(height: 16),

              _composeCard(),

              const SizedBox(height: 16),

              _quickStats(),

            ],

          ),

        );

      }


      Widget _headerCard() {

        return Card(

          elevation: 8,

          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),

          child: Padding(

            padding: const EdgeInsets.all(18),

            child: Row(

              children: [

                Container(

                  width: 56,

                  height: 56,

                  decoration: BoxDecoration(

                    gradient: const LinearGradient(

                      colors: [Colors.blue, Colors.lightBlueAccent],

                    ),

                    borderRadius: BorderRadius.circular(18),

                  ),

                  child: const Icon(Icons.chat_bubble_rounded, color: Colors.white),

                ),

                const SizedBox(width: 14),

                const Expanded(

                  child: Column(

                    crossAxisAlignment: CrossAxisAlignment.start,

                    children: [

                      Text(

                        'WhatsApp Sender Pro',

                        style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),

                      ),

                      SizedBox(height: 4),

                      Text('Kirim pesan, simpan favorit, dan cek riwayat dengan cepat.'),

                    ],

                  ),

                ),

              ],

            ),

          ),

        );

      }


      Widget _composeCard() {

        final currentPreview = '${selectedCountry.flag} ${selectedCountry.name} ${selectedCountry.code}';

        return Card(

          elevation: 10,

          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),

          child: Padding(

            padding: const EdgeInsets.all(18),

            child: Column(

              crossAxisAlignment: CrossAxisAlignment.stretch,

              children: [

                Row(

                  children: [

                    Expanded(

                      child: OutlinedButton.icon(

                        onPressed: openCountryPicker,

                        icon: const Icon(Icons.public),

                        label: Text(currentPreview, overflow: TextOverflow.ellipsis),

                      ),

                    ),

                  ],

                ),

                const SizedBox(height: 14),

                TextField(

                  controller: phoneController,

                  keyboardType: TextInputType.phone,

                  decoration: const InputDecoration(

                    labelText: 'Nomor WhatsApp',

                    hintText: '812xxxxxxx',

                    border: OutlineInputBorder(),

                  ),

                ),

                const SizedBox(height: 12),

                TextField(

                  controller: favoriteLabelController,

                  decoration: const InputDecoration(

                    labelText: 'Label favorit (opsional)',

                    hintText: 'Contoh: Temen sekolah',

                    border: OutlineInputBorder(),

                  ),

                ),

                const SizedBox(height: 12),

                TextField(

                  controller: messageController,

                  maxLines: 4,

                  decoration: InputDecoration(

                    labelText: 'Pesan',

                    hintText: 'Tulis pesan yang mau dikirim...',

                    border: const OutlineInputBorder(),

                    suffixIcon: IconButton(

                      tooltip: 'Toggle preview mode',

                      icon: Icon(useBoldMessagePreview ? Icons.visibility : Icons.visibility_off),

                      onPressed: () => setState(() => useBoldMessagePreview = !useBoldMessagePreview),

                    ),

                  ),

                ),

                const SizedBox(height: 14),

                Wrap(

                  spacing: 10,

                  runSpacing: 10,

                  children: [

                    FilledButton.icon(

                      onPressed: sendWhatsApp,

                      icon: const Icon(Icons.send),

                      label: const Text('Kirim'),

                    ),

                    OutlinedButton.icon(

                      onPressed: clearAll,

                      icon: const Icon(Icons.delete_outline),

                      label: const Text('Clear'),

                    ),

                    OutlinedButton.icon(

                      onPressed: openChatWithoutMessage,

                      icon: const Icon(Icons.chat),

                      label: const Text('Chat kosong'),

                    ),

                    OutlinedButton.icon(

                      onPressed: openApiLink,

                      icon: const Icon(Icons.link),

                      label: const Text('API link'),

                    ),

                    FilledButton.tonalIcon(

                      onPressed: saveFavorite,

                      icon: const Icon(Icons.star_border),

                      label: const Text('Simpan favorit'),

                    ),

                  ],

                ),

                const SizedBox(height: 10),

                if (useBoldMessagePreview && messageController.text.trim().isNotEmpty)

                  Container(

                    padding: const EdgeInsets.all(12),

                    decoration: BoxDecoration(

                      color: Colors.blue.withOpacity(0.08),

                      borderRadius: BorderRadius.circular(16),

                    ),

                    child: Text(

                      'Preview: ${messageController.text.trim()}',

                      style: const TextStyle(fontWeight: FontWeight.w600),

                    ),

                  ),

              ],

            ),

          ),

        );

      }


      Widget _quickStats() {

        return Row(

          children: [

            Expanded(

              child: _statCard('Favorit', favorites.length.toString(), Icons.star),

            ),

            const SizedBox(width: 12),

            Expanded(

              child: _statCard('Riwayat', history.length.toString(), Icons.history),

            ),

          ],

        );

      }


      Widget _statCard(String label, String value, IconData icon) {

        return Card(

          elevation: 6,

          shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),

          child: Padding(

            padding: const EdgeInsets.all(16),

            child: Row(

              children: [

                Icon(icon, size: 30),

                const SizedBox(width: 12),

                Column(

                  crossAxisAlignment: CrossAxisAlignment.start,

                  children: [

                    Text(value, style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),

                    Text(label),

                  ],

                ),

              ],

            ),

          ),

        );

      }


      Widget buildFavorites() {

        return SingleChildScrollView(

          padding: const EdgeInsets.all(16),

          child: Column(

            crossAxisAlignment: CrossAxisAlignment.stretch,

            children: [

              Card(

                elevation: 8,

                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),

                child: Padding(

                  padding: const EdgeInsets.all(18),

                  child: Row(

                    children: [

                      const Icon(Icons.star, size: 30),

                      const SizedBox(width: 12),

                      Expanded(

                        child: Text(

                          'Nomor Favorit',

                          style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),

                        ),

                      ),

                      TextButton.icon(

                        onPressed: favorites.isEmpty ? null : () {

                          favorites.clear();

                          _saveFavorites(); // Uses the safe helper to save the now-empty list

                          setState(() {});

                        },

                        icon: const Icon(Icons.delete_outline),

                        label: const Text('Hapus semua'),

                      ),

                    ],

                  ),

                ),

              ),

              const SizedBox(height: 12),

              if (favorites.isEmpty)

                const Padding(

                  padding: EdgeInsets.all(20),

                  child: Center(child: Text('Belum ada nomor favorit disimpan.')),

                )

              else

                ...favorites.map((fav) {

                  return Card(

                    elevation: 4,

                    margin: const EdgeInsets.only(bottom: 12),

                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),

                    child: ListTile(

                      leading: CircleAvatar(child: Text(fav.flag)),

                      title: Text(fav.label),

                      subtitle: Text('${fav.countryName} • ${fav.countryCode}${fav.phone}'),

                      onTap: () => loadFavorite(fav),

                      trailing: IconButton(

                        icon: const Icon(Icons.delete_forever),

                        onPressed: () => deleteFavorite(fav),

                      ),

                    ),

                  );

                }),

            ],

          ),

        );

      }


      Widget buildHistory() {

        return SingleChildScrollView(

          padding: const EdgeInsets.all(16),

          child: Column(

            crossAxisAlignment: CrossAxisAlignment.stretch,

            children: [

              Card(

                elevation: 8,

                shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),

                child: Padding(

                  padding: const EdgeInsets.all(18),

                  child: Row(

                    children: [

                      const Icon(Icons.history, size: 30),

                      const SizedBox(width: 12),

                      Expanded(

                        child: Text(

                          'History Pesan',

                          style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),

                        ),

                      ),

                      TextButton.icon(

                        onPressed: history.isEmpty ? null : clearHistory,

                        icon: const Icon(Icons.delete_outline),

                        label: const Text('Bersihkan'),

                      ),

                    ],

                  ),

                ),

              ),

              const SizedBox(height: 12),

              if (history.isEmpty)

                const Padding(

                  padding: EdgeInsets.all(20),

                  child: Center(child: Text('Belum ada riwayat pesan.')),

                )

              else

                ...history.map((item) {

                  return Card(

                    elevation: 4,

                    margin: const EdgeInsets.only(bottom: 12),

                    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),

                    child: ListTile(

                      leading: CircleAvatar(child: Text(item.flag)),

                      title: Text('${item.countryCode}${item.phone}'),

                      subtitle: Text(

                        '${item.countryName}\n${item.message}',

                        maxLines: 3,

                        overflow: TextOverflow.ellipsis,

                      ),

                      isThreeLine: true,

                      trailing: Text(

                        '${item.time.hour.toString().padLeft(2, '0')}:${item.time.minute.toString().padLeft(2, '0')}',

                      ),

                    ),

                  );

                }),

            ],

          ),

        );

      }


      @override

      Widget build(BuildContext context) {

        final pages = [buildDashboard(), buildFavorites(), buildHistory()];


        return Scaffold(

          appBar: AppBar(

            title: const Text('WhatsApp Sender Pro'),

            centerTitle: true,

            actions: [

              IconButton(

                icon: Icon(isDarkMode ? Icons.light_mode : Icons.dark_mode),

                onPressed: () => setState(() => isDarkMode = !isDarkMode),

              ),

            ],

          ),

          body: Container(

            decoration: BoxDecoration(

              gradient: LinearGradient(

                colors: isDarkMode

                    ? [Colors.black, Colors.blueGrey.shade900]

                    : [Colors.blue.shade300, Colors.blue.shade50],

                begin: Alignment.topLeft,

                end: Alignment.bottomRight,

              ),

            ),

            child: IndexedStack(index: selectedTab, children: pages),

          ),

          bottomNavigationBar: NavigationBar(

            selectedIndex: selectedTab,

            onDestinationSelected: (value) => setState(() => selectedTab = value),

            destinations: const [

              NavigationDestination(icon: Icon(Icons.message_outlined), selectedIcon: Icon(Icons.message), label: 'Kirim'),

              NavigationDestination(icon: Icon(Icons.star_border), selectedIcon: Icon(Icons.star), label: 'Favorit'),

              NavigationDestination(icon: Icon(Icons.history_outlined), selectedIcon: Icon(Icons.history), label: 'Riwayat'),

            ],

          ),

        );

      }

    }

  3. Klik Run

  4. Langsung jadi web app!


🔥 Next Improvement (Kalau mau upgrade lagi)

Kalau mau lebih gila lagi, bisa ditambah:

  • 📂 Import CSV (bulk kirim)

  • 🤖 Auto message scheduler

  • 📱 UI mirip WhatsApp asli

  • 🔔 Notifikasi sukses kirim

  • 🔗 Short link generator


🎯 Kesimpulan

Project ini cocok banget buat:

  • Latihan Flutter Web

  • Portfolio sekolah / SMK

  • Tools pribadi sehari-hari

Simple tapi powerful 🔥


Nama: Rizqi Resdhiana XI RPL II

Comments

Popular posts from this blog

Jenis-jenis sistem operasi os

Latihan membuat tampilan flutter sederhana 🤖

Belajar Flutter