Tampilan Login page keren dan bagus namun simple 📹


📺 AmbaTube: Login Page Kekinian ala YouTube Style

Halo coder squad! 👋
Kalau biasanya kita bikin halaman login ala kadarnya (text field → tombol → selesai), kali ini kita bakal bikin auth page yang vibes-nya mirip YouTube/Google style. Jadi bukan sekadar “bisa login”, tapi juga punya animasi halus, desain elegan, dan UX yang proper. 🔥


🔑 Fitur Keren di AmbaTube Auth

  • Login dengan Email & Password → Ada validasi email dan minimal 8 karakter password.

  • Sign Up Page → Form daftar lengkap dengan konfirmasi password, plus indikator apakah password udah match atau belum.

  • Forgot Password → Halaman khusus buat reset password (simulasi, tinggal sambung ke Firebase kalau mau beneran).

  • Animasi Intro & Transition → Logo nge-pop up elastis, form slide in smooth, tombol berubah warna kalau valid.

  • Social Login Button (Google & Apple) → Biar makin modern dan familiar buat user zaman now.


🎨 Tampilan UI

Gaya desainnya sengaja dibikin mirip YouTube:

  • Warna dominan merah khas YouTube.

  • Logo dengan ikon play button + tulisan AmbaTube.

  • Input field dengan border rounded + background abu-abu soft.

  • Animasi smooth pas masuk halaman, biar user nggak bosan.

  • Tombol login dengan state aktif/non-aktif (interaktif banget).


💻 Codenya

Silakan copas ke main.dart Ya bro:

import 'package:flutter/material.dart';

void main() {
  runApp(const YouTubeStyleAuthApp());
}

/// Ubah nama aplikasimu di sini kalau perlu
const String appName = 'AmbaTube';

class YouTubeStyleAuthApp extends StatelessWidget {
  const YouTubeStyleAuthApp({super.key});

  @override
  Widget build(BuildContext context) {
    final primary = Colors.red.shade700;
    return MaterialApp(
      title: appName,
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        useMaterial3: true,
        colorScheme: ColorScheme.fromSeed(seedColor: primary),
        scaffoldBackgroundColor: Colors.white,
      ),
      home: const LoginPage(),
    );
  }
}

/// -----------------------------
/// Login Page
/// -----------------------------
class LoginPage extends StatefulWidget {
  const LoginPage({super.key});
  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> with SingleTickerProviderStateMixin {
  final _emailCtrl = TextEditingController();
  final _passwordCtrl = TextEditingController();
  bool _obscure = true;

  late final AnimationController _introController;
  late final Animation<double> _logoScale;
  late final Animation<double> _logoFade;
  late final Animation<Offset> _formOffset;
  late final Animation<double> _formFade;

  final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+$');

  bool get _emailValid => emailRegex.hasMatch(_emailCtrl.text.trim());
  bool get _passwordValid => _passwordCtrl.text.length >= 8;

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

    _emailCtrl.addListener(() => setState(() {}));
    _passwordCtrl.addListener(() => setState(() {}));

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

    _logoScale = Tween<double>(begin: 0.7, end: 1.0).animate(
      CurvedAnimation(parent: _introController, curve: const Interval(0.0, 0.45, curve: Curves.elasticOut)),
    );
    _logoFade = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _introController, curve: const Interval(0.0, 0.45, curve: Curves.easeOut)),
    );

    _formOffset = Tween<Offset>(begin: const Offset(0, 0.18), end: Offset.zero).animate(
      CurvedAnimation(parent: _introController, curve: const Interval(0.35, 1.0, curve: Curves.easeOut)),
    );
    _formFade = Tween<double>(begin: 0.0, end: 1.0).animate(
      CurvedAnimation(parent: _introController, curve: const Interval(0.35, 1.0, curve: Curves.easeIn)),
    );

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) _introController.forward();
    });
  }

  @override
  void dispose() {
    _introController.dispose();
    _emailCtrl.dispose();
    _passwordCtrl.dispose();
    super.dispose();
  }

  void _onContinue() {
    if (_emailValid && _passwordValid) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Login sukses (contoh).')));
    } else {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Email atau password tidak valid.')));
    }
  }

  Route _createRoute(Widget page) {
    return PageRouteBuilder(
      transitionDuration: const Duration(milliseconds: 450),
      reverseTransitionDuration: const Duration(milliseconds: 380),
      pageBuilder: (context, animation, secondaryAnimation) => page,
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        final offsetAnim = Tween<Offset>(begin: const Offset(0.2, 0), end: Offset.zero)
            .animate(CurvedAnimation(parent: animation, curve: Curves.easeOut));
        final fadeAnim = CurvedAnimation(parent: animation, curve: Curves.easeOut);
        return FadeTransition(
          opacity: fadeAnim,
          child: SlideTransition(position: offsetAnim, child: child),
        );
      },
    );
  }

  Widget _logoWidget() {
    return Hero(
      tag: 'yt-logo-login',
      child: Material(
        color: Colors.transparent,
        child: AnimatedBuilder(
          animation: _introController,
          builder: (context, _) {
            return Opacity(
              opacity: _logoFade.value,
              child: Transform.scale(
                scale: _logoScale.value,
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  children: [
                    Container(
                      width: 50,
                      height: 36,
                      decoration: BoxDecoration(
                        color: Colors.red.shade700,
                        borderRadius: BorderRadius.circular(6),
                        boxShadow: [
                          BoxShadow(
                            color: Colors.black.withOpacity(0.08),
                            blurRadius: 6,
                            offset: const Offset(0, 4),
                          )
                        ],
                      ),
                      child: const Center(child: Icon(Icons.play_arrow, color: Colors.white, size: 22)),
                    ),
                    const SizedBox(width: 12),
                    Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          appName,
                          style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: Colors.grey[900]),
                        ),
                        const SizedBox(height: 2),
                        Text('Work without limits', style: TextStyle(fontSize: 12, color: Colors.grey[600])),
                      ],
                    )
                  ],
                ),
              ),
            );
          },
        ),
      ),
    );
  }

  InputDecoration _decor({required String hint, Widget? suffix}) {
    return InputDecoration(
      hintText: hint,
      filled: true,
      fillColor: Colors.grey[100],
      contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(28),
        borderSide: BorderSide(color: Colors.grey.shade300),
      ),
      focusedBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(28),
        borderSide: BorderSide(color: Colors.red.shade700),
      ),
      suffixIcon: suffix,
    );
  }

  @override
  Widget build(BuildContext context) {
    final maxWidth = 420.0;
    final enabled = _emailValid && _passwordValid;

    return Scaffold(
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 26),
            child: ConstrainedBox(
              constraints: BoxConstraints(maxWidth: maxWidth),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  _logoWidget(),
                  const SizedBox(height: 28),

                  AnimatedBuilder(
                    animation: _introController,
                    builder: (context, _) {
                      return Opacity(
                        opacity: _formFade.value,
                        child: SlideTransition(position: _formOffset, child: _buildCard(enabled)),
                      );
                    },
                  ),

                  const SizedBox(height: 18),
                  Row(children: const [
                    Expanded(child: Divider()),
                    Padding(padding: EdgeInsets.symmetric(horizontal: 12), child: Text('or')),
                    Expanded(child: Divider()),
                  ]),
                  const SizedBox(height: 12),

                  SizedBox(
                    width: double.infinity,
                    child: OutlinedButton.icon(
                      onPressed: () => ScaffoldMessenger.of(context)
                          .showSnackBar(const SnackBar(content: Text('Sign in with Google (contoh).'))),
                      icon: Container(
                        width: 28,
                        height: 28,
                        alignment: Alignment.center,
                        child: const Text(
                          'G',
                          style: TextStyle(fontWeight: FontWeight.bold, color: Colors.red),
                        ),
                      ),
                      label: const Text('Sign up with Google', style: TextStyle(fontWeight: FontWeight.w600)),
                      style: OutlinedButton.styleFrom(
                        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
                        padding: const EdgeInsets.symmetric(vertical: 14),
                      ),
                    ),
                  ),
                  const SizedBox(height: 10),
                  SizedBox(
                    width: double.infinity,
                    child: OutlinedButton.icon(
                      onPressed: () => ScaffoldMessenger.of(context)
                          .showSnackBar(const SnackBar(content: Text('Sign in with Apple (contoh).'))),
                      icon: const Icon(Icons.apple),
                      label: const Text('Sign up with Apple', style: TextStyle(fontWeight: FontWeight.w600)),
                      style: OutlinedButton.styleFrom(
                        shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(30)),
                        padding: const EdgeInsets.symmetric(vertical: 14),
                      ),
                    ),
                  ),

                  const SizedBox(height: 18),
                  Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Text("Don't have an account?", style: TextStyle(color: Colors.grey[700])),
                      TextButton(
                        onPressed: () {
                          Navigator.of(context).push(_createRoute(const SignUpPage()));
                        },
                        child: const Text('Sign up', style: TextStyle(fontWeight: FontWeight.w700)),
                      ),
                    ],
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }

  Widget _buildCard(bool enabled) {
    return Container(
      padding: const EdgeInsets.all(18),
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.circular(16),
        boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 18, offset: const Offset(0, 8))],
      ),
      child: Column(
        children: [
          TextField(
            controller: _emailCtrl,
            keyboardType: TextInputType.emailAddress,
            decoration: _decor(hint: 'Your email address'),
          ),
          const SizedBox(height: 12),

          TextField(
            controller: _passwordCtrl,
            obscureText: _obscure,
            decoration: _decor(
              hint: 'Choose a password',
              suffix: IconButton(
                icon: Icon(_obscure ? Icons.visibility_off : Icons.visibility, color: Colors.grey[600]),
                onPressed: () => setState(() => _obscure = !_obscure),
              ),
            ),
          ),
          const SizedBox(height: 8),
          Row(
            children: [
              Text('min. 8 characters', style: TextStyle(color: Colors.grey[500], fontSize: 12)),
              const Spacer(),
              AnimatedOpacity(
                duration: const Duration(milliseconds: 220),
                opacity: _passwordValid ? 1.0 : 0.0,
                child: const Icon(Icons.check_circle, color: Colors.green, size: 18),
              ),
            ],
          ),
          const SizedBox(height: 16),

          AnimatedContainer(
            duration: const Duration(milliseconds: 250),
            curve: Curves.easeInOut,
            width: double.infinity,
            height: 52,
            decoration: BoxDecoration(
              color: enabled ? Colors.red.shade700 : Colors.grey.shade300,
              borderRadius: BorderRadius.circular(30),
              boxShadow: enabled
                  ? [BoxShadow(color: Colors.red.shade700.withOpacity(0.18), blurRadius: 8, offset: const Offset(0, 4))]
                  : null,
            ),
            child: Material(
              color: Colors.transparent,
              child: InkWell(
                borderRadius: BorderRadius.circular(30),
                onTap: enabled ? _onContinue : null,
                child: Center(
                  child: AnimatedDefaultTextStyle(
                    duration: const Duration(milliseconds: 200),
                    style: TextStyle(
                      color: enabled ? Colors.white : Colors.grey[700],
                      fontWeight: FontWeight.w600,
                      fontSize: 16,
                    ),
                    child: const Text('Continue'),
                  ),
                ),
              ),
            ),
          ),

          const SizedBox(height: 8),
          Align(
            alignment: Alignment.centerRight,
            child: TextButton(
              onPressed: () => Navigator.of(context).push(_createRoute(const ForgotPasswordPage())),
              child: const Text('Forgot password?'),
            ),
          ),
        ],
      ),
    );
  }
}

/// -----------------------------
/// Sign Up Page
/// -----------------------------
class SignUpPage extends StatefulWidget {
  const SignUpPage({super.key});
  @override
  State<SignUpPage> createState() => _SignUpPageState();
}

class _SignUpPageState extends State<SignUpPage> with SingleTickerProviderStateMixin {
  final _nameCtrl = TextEditingController();
  final _emailCtrl = TextEditingController();
  final _passwordCtrl = TextEditingController();
  final _confirmCtrl = TextEditingController();
  bool _obscure = true;
  bool _obscureConfirm = true;

  late final AnimationController _controller;
  late final Animation<double> _logoFade;
  late final Animation<Offset> _formOffset;
  late final Animation<double> _formFade;

  final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+$');

  bool get _nameValid => _nameCtrl.text.trim().length >= 2;
  bool get _emailValid => emailRegex.hasMatch(_emailCtrl.text.trim());
  bool get _passwordValid => _passwordCtrl.text.length >= 8;
  bool get _confirmValid => _confirmCtrl.text == _passwordCtrl.text && _confirmCtrl.text.isNotEmpty;

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

    _nameCtrl.addListener(() => setState(() {}));
    _emailCtrl.addListener(() => setState(() {}));
    _passwordCtrl.addListener(() => setState(() {}));
    _confirmCtrl.addListener(() => setState(() {}));

    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 640));
    _logoFade = Tween<double>(begin: 0.0, end: 1.0)
        .animate(CurvedAnimation(parent: _controller, curve: const Interval(0.0, 0.35, curve: Curves.easeIn)));
    _formOffset = Tween<Offset>(begin: const Offset(0, 0.12), end: Offset.zero)
        .animate(CurvedAnimation(parent: _controller, curve: const Interval(0.25, 1.0, curve: Curves.easeOut)));
    _formFade = Tween<double>(begin: 0.0, end: 1.0)
        .animate(CurvedAnimation(parent: _controller, curve: const Interval(0.25, 1.0, curve: Curves.easeIn)));

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) _controller.forward();
    });
  }

  @override
  void dispose() {
    _controller.dispose();
    _nameCtrl.dispose();
    _emailCtrl.dispose();
    _passwordCtrl.dispose();
    _confirmCtrl.dispose();
    super.dispose();
  }

  void _onSignUp() {
    if (_nameValid && _emailValid && _passwordValid && _confirmValid) {
      ScaffoldMessenger.of(context)
          .showSnackBar(const SnackBar(content: Text('Account created successfully (contoh).')));
      Navigator.of(context).pop();
    } else {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Form belum valid.')));
    }
  }

  InputDecoration _dec({required String hint, Widget? suffix}) {
    return InputDecoration(
      hintText: hint,
      filled: true,
      fillColor: Colors.grey[100],
      contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(28),
        borderSide: BorderSide(color: Colors.grey.shade300),
      ),
      focusedBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(28),
        borderSide: BorderSide(color: Colors.red.shade700),
      ),
      suffixIcon: suffix,
    );
  }

  @override
  Widget build(BuildContext context) {
    final maxWidth = 420.0;
    final canSign = _nameValid && _emailValid && _passwordValid && _confirmValid;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Create account'),
        backgroundColor: Colors.white,
        elevation: 0,
        foregroundColor: Colors.black87,
      ),
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
            child: ConstrainedBox(
              constraints: BoxConstraints(maxWidth: maxWidth),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Hero(
                    tag: 'yt-logo-signup',
                    child: FadeTransition(
                      opacity: _logoFade,
                      child: Material(
                        color: Colors.transparent,
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Container(
                              width: 46,
                              height: 32,
                              decoration: BoxDecoration(
                                color: Colors.red.shade700,
                                borderRadius: BorderRadius.circular(6),
                              ),
                              child: const Center(child: Icon(Icons.play_arrow, color: Colors.white, size: 20)),
                            ),
                            const SizedBox(width: 12),
                            Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(
                                  appName,
                                  style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: Colors.grey[900]),
                                ),
                                const SizedBox(height: 2),
                                Text('Create your account', style: TextStyle(fontSize: 12, color: Colors.grey[600])),
                              ],
                            )
                          ],
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(height: 18),

                  SlideTransition(
                    position: _formOffset,
                    child: FadeTransition(
                      opacity: _formFade,
                      child: Column(
                        children: [
                          TextField(controller: _nameCtrl, decoration: _dec(hint: 'Full name')),
                          const SizedBox(height: 12),
                          TextField(
                            controller: _emailCtrl,
                            keyboardType: TextInputType.emailAddress,
                            decoration: _dec(hint: 'Email address'),
                          ),
                          const SizedBox(height: 12),
                          TextField(
                            controller: _passwordCtrl,
                            obscureText: _obscure,
                            decoration: _dec(
                              hint: 'Password (min 8 chars)',
                              suffix: IconButton(
                                icon: Icon(_obscure ? Icons.visibility_off : Icons.visibility, color: Colors.grey[600]),
                                onPressed: () => setState(() => _obscure = !_obscure),
                              ),
                            ),
                          ),
                          const SizedBox(height: 12),
                          TextField(
                            controller: _confirmCtrl,
                            obscureText: _obscureConfirm,
                            decoration: _dec(
                              hint: 'Confirm password',
                              suffix: IconButton(
                                icon: Icon(_obscureConfirm ? Icons.visibility_off : Icons.visibility, color: Colors.grey[600]),
                                onPressed: () => setState(() => _obscureConfirm = !_obscureConfirm),
                              ),
                            ),
                          ),
                          const SizedBox(height: 16),
                          Row(
                            children: [
                              if (_passwordValid)
                                const Icon(Icons.check_circle, color: Colors.green, size: 18),
                              if (_passwordValid) const SizedBox(width: 8),
                              Text('min. 8 characters', style: TextStyle(color: Colors.grey[600])),
                              const Spacer(),
                              AnimatedDefaultTextStyle(
                                duration: const Duration(milliseconds: 200),
                                style: TextStyle(color: _confirmValid ? Colors.green : Colors.red),
                                child: Text(_confirmValid ? 'passwords match' : "passwords don't match"),
                              ),
                            ],
                          ),
                          const SizedBox(height: 18),

                          AnimatedContainer(
                            duration: const Duration(milliseconds: 240),
                            width: double.infinity,
                            height: 52,
                            decoration: BoxDecoration(
                              color: canSign ? Colors.red.shade700 : Colors.grey.shade300,
                              borderRadius: BorderRadius.circular(30),
                            ),
                            child: Material(
                              color: Colors.transparent,
                              child: InkWell(
                                borderRadius: BorderRadius.circular(30),
                                onTap: canSign ? _onSignUp : null,
                                child: Center(
                                  child: AnimatedDefaultTextStyle(
                                    duration: const Duration(milliseconds: 200),
                                    style: TextStyle(
                                      color: canSign ? Colors.white : Colors.grey[700],
                                      fontWeight: FontWeight.w600,
                                      fontSize: 16,
                                    ),
                                    child: const Text('Sign up'),
                                  ),
                                ),
                              ),
                            ),
                          ),
                          const SizedBox(height: 12),
                          Row(
                            mainAxisAlignment: MainAxisAlignment.center,
                            children: [
                              const Text('Already have an account?'),
                              TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text('Sign in')),
                            ],
                          ),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

/// -----------------------------
/// Forgot Password Page
/// -----------------------------
class ForgotPasswordPage extends StatefulWidget {
  const ForgotPasswordPage({super.key});
  @override
  State<ForgotPasswordPage> createState() => _ForgotPasswordPageState();
}

class _ForgotPasswordPageState extends State<ForgotPasswordPage> with SingleTickerProviderStateMixin {
  final _emailCtrl = TextEditingController();
  bool _loading = false;

  late final AnimationController _controller;
  late final Animation<double> _logoFade;
  late final Animation<Offset> _formOffset;
  late final Animation<double> _formFade;

  final emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+$');
  bool get _emailValid => emailRegex.hasMatch(_emailCtrl.text.trim());

  @override
  void initState() {
    super.initState();
    _emailCtrl.addListener(() => setState(() {}));

    _controller = AnimationController(vsync: this, duration: const Duration(milliseconds: 620));
    _logoFade = Tween<double>(begin: 0.0, end: 1.0)
        .animate(CurvedAnimation(parent: _controller, curve: const Interval(0.0, 0.35, curve: Curves.easeIn)));
    _formOffset = Tween<Offset>(begin: const Offset(0, 0.08), end: Offset.zero)
        .animate(CurvedAnimation(parent: _controller, curve: const Interval(0.18, 1.0, curve: Curves.easeOut)));
    _formFade = Tween<double>(begin: 0.0, end: 1.0)
        .animate(CurvedAnimation(parent: _controller, curve: const Interval(0.18, 1.0, curve: Curves.easeIn)));

    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) _controller.forward();
    });
  }

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

  Future<void> _sendReset() async {
    final email = _emailCtrl.text.trim();
    if (!_emailValid) {
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Masukkan email yang valid.')));
      return;
    }

    setState(() => _loading = true);

    try {
      // MOCK kirim email
      await Future.delayed(const Duration(milliseconds: 900));
      if (!mounted) return;
      setState(() => _loading = false);

      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('Reset link sent'),
          content: Text(
            'Kami telah mengirim tautan reset password ke:\n\n$email\n\n(Ini simulasi. Ganti dengan integrasi Firebase untuk produksi.)',
          ),
          actions: [
            TextButton(
              onPressed: () {
                Navigator.of(context).pop(); // tutup dialog
                Navigator.of(context).pop(); // kembali ke login
              },
              child: const Text('OK'),
            ),
          ],
        ),
      );
    } catch (_) {
      if (!mounted) return;
      setState(() => _loading = false);
      ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Gagal mengirim. Coba lagi nanti.')));
    }
  }

  InputDecoration _dec({required String hint}) {
    return InputDecoration(
      hintText: hint,
      filled: true,
      fillColor: Colors.grey[100],
      contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
      border: OutlineInputBorder(
        borderRadius: BorderRadius.circular(28),
        borderSide: BorderSide(color: Colors.grey.shade300),
      ),
      focusedBorder: OutlineInputBorder(
        borderRadius: BorderRadius.circular(28),
        borderSide: BorderSide(color: Colors.red.shade700),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    final maxWidth = 420.0;
    final canSend = _emailValid && !_loading;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Reset password'),
        backgroundColor: Colors.white,
        elevation: 0,
        foregroundColor: Colors.black87,
      ),
      body: SafeArea(
        child: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
            child: ConstrainedBox(
              constraints: BoxConstraints(maxWidth: maxWidth),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Hero(
                    tag: 'yt-logo-forgot',
                    child: FadeTransition(
                      opacity: _logoFade,
                      child: Material(
                        color: Colors.transparent,
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Container(
                              width: 46,
                              height: 32,
                              decoration: BoxDecoration(color: Colors.red.shade700, borderRadius: BorderRadius.circular(6)),
                              child: const Center(child: Icon(Icons.play_arrow, color: Colors.white, size: 20)),
                            ),
                            const SizedBox(width: 12),
                            Column(
                              crossAxisAlignment: CrossAxisAlignment.start,
                              children: [
                                Text(appName, style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700, color: Colors.grey[900])),
                                const SizedBox(height: 2),
                                Text('Reset your password', style: TextStyle(fontSize: 12, color: Colors.grey[600])),
                              ],
                            )
                          ],
                        ),
                      ),
                    ),
                  ),
                  const SizedBox(height: 18),

                  SlideTransition(
                    position: _formOffset,
                    child: FadeTransition(
                      opacity: _formFade,
                      child: Column(
                        children: [
                          TextField(
                            controller: _emailCtrl,
                            keyboardType: TextInputType.emailAddress,
                            decoration: _dec(hint: 'Enter your email address'),
                          ),
                          const SizedBox(height: 14),

                          AnimatedContainer(
                            duration: const Duration(milliseconds: 240),
                            width: double.infinity,
                            height: 52,
                            decoration: BoxDecoration(
                              color: canSend ? Colors.red.shade700 : Colors.grey.shade300,
                              borderRadius: BorderRadius.circular(30),
                            ),
                            child: Material(
                              color: Colors.transparent,
                              child: InkWell(
                                borderRadius: BorderRadius.circular(30),
                                onTap: canSend ? _sendReset : null,
                                child: Center(
                                  child: _loading
                                      ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2.2))
                                      : AnimatedDefaultTextStyle(
                                          duration: const Duration(milliseconds: 200),
                                          style: TextStyle(
                                            color: canSend ? Colors.white : Colors.grey[700],
                                            fontWeight: FontWeight.w600,
                                            fontSize: 16,
                                          ),
                                          child: const Text('Send reset link'),
                                        ),
                                ),
                              ),
                            ),
                          ),
                          const SizedBox(height: 12),
                          Text(
                            'We will send a password reset link to your email if it exists in our system.',
                            style: TextStyle(color: Colors.grey[600], fontSize: 12),
                          ),
                        ],
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

🚀 Kenapa Harus Cobain AmbaTube Style?

  1. Lebih Realistis → Desain login kayak gini lebih deket ke aplikasi modern yang sering dipake sehari-hari.

  2. Siap Produksi → Tinggal sambungin ke Firebase Auth atau backend kamu, langsung jadi auth system beneran.

  3. Belajar Animasi Flutter → Bukan cuma UI statis, kamu juga belajar bikin animasi halus dengan AnimationController.

  4. User Friendly → Validasi langsung di form bikin pengalaman user lebih smooth (anti password salah mulu).


🎯 Penutup

Itu dia template login–sign up AmbaTube yang vibes-nya modern, simpel, tapi tetap powerful. Cocok banget buat project latihan atau bahkan base project beneran.

Jangan lupa, ini baru mock UI. Kalau mau beneran jalanin social login atau reset password, kamu bisa lanjut integrasiin dengan Firebase Authentication.

So… siap bikin app yang gak cuma “bisa jalan” tapi juga “bisa dipamerin”? 🚀
Gaskeun coding, coder squad! 🔥

MADE BY: RIZQI RESDHIANA XI RPL 2

Review: https://zwhq06g8whr0.zapp.page/#/



Comments

Popular posts from this blog

Jenis-jenis sistem operasi os

Latihan membuat tampilan flutter sederhana 🤖

Belajar Flutter