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),
),
],
),
),
),
],
),
),
),
),
),
);
}
}
Comments
Post a Comment