31 lines
40 KiB
Plaintext
31 lines
40 KiB
Plaintext
<start of data/mock_products.dart>
|
|
import '../models/product_model.dart'; // Import your Product model final List<Product> mockProducts = [ Product( id: '1', name: 'Nighthawk AX12 Router', description: 'Blazing fast Wi-Fi 6 router for demanding networks and gaming.', // Using Picsum Photos: https://picsum.photos/ // The numbers are width/height. The ?random=1 ensures a different image. imageUrl: 'https://picsum.photos/seed/router1/400/300', price: 299.99, category: 'Routers', ), Product( id: '2', name: 'MeshForce M3s Wi-Fi System', description: 'Whole-home mesh Wi-Fi system for seamless coverage up to 6000 sq ft.', imageUrl: 'https://picsum.photos/seed/mesh1/400/300', price: 179.00, category: 'Mesh Systems', ), Product( id: '3', name: 'TP-Link 8 Port Gigabit Switch', description: 'Expand your wired network with this reliable and easy-to-use switch.', imageUrl: 'https://picsum.photos/seed/switch1/400/300', price: 24.99, category: 'Switches', ), Product( id: '4', name: 'Arris Surfboard SB8200 Modem', description: 'DOCSIS 3.1 cable modem for high-speed internet plans.', imageUrl: 'https://picsum.photos/seed/modem1/400/300', price: 149.99, category: 'Modems', ), Product( id: '5', name: 'Ubiquiti EdgeRouter X', description: 'Advanced SOHO router with powerful features and compact design.', imageUrl: 'https://picsum.photos/seed/edgerouter/400/300', price: 59.00, category: 'Routers', ), Product( id: '6', name: 'Netgear Orbi RBK752', description: 'Tri-band Mesh WiFi 6 System, reliable coverage for your smart home.', imageUrl: 'https://picsum.photos/seed/orbi1/400/300', price: 379.99, category: 'Mesh Systems', ), Product( id: '7', name: 'Ethernet Network Cable 50ft', description: 'Cat 6 Ethernet cable for high-speed wired connections.', imageUrl: 'https://picsum.photos/seed/cable1/400/300', price: 12.99, category: 'Accessories', ), Product( id: '8', name: 'Raspberry Pi 4 Model B', description: 'Versatile single-board computer, perfect for network projects.', imageUrl: 'https://picsum.photos/seed/rpi4/400/300', price: 75.00, category: 'DIY Networking', ), ];
|
|
<end of data/mock_products.dart>
|
|
|
|
<start of forgot_password_page.dart>
|
|
import 'package:flutter/material.dart'; class ForgotPasswordPage extends StatefulWidget { const ForgotPasswordPage({super.key}); @override State<ForgotPasswordPage> createState() => _ForgotPasswordPageState(); } class _ForgotPasswordPageState extends State<ForgotPasswordPage> { final _formKey = GlobalKey<FormState>(); final TextEditingController _emailController = TextEditingController(); bool _isLoading = false; String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Please enter your email'; } if (!RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+").hasMatch(value)) { return 'Please enter a valid email'; } return null; } Future<void> _sendResetLink() async { if (_formKey.currentState!.validate()) { setState(() { _isLoading = true; }); // Simulate network delay await Future.delayed(const Duration(seconds: 1)); final email = _emailController.text; setState(() { _isLoading = false; }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('If an account exists for $email, a reset link has been mock-sent.'), duration: const Duration(seconds: 3), ), ); // Optionally navigate back after showing the message await Future.delayed(const Duration(seconds: 1)); // Give time to read snackbar if (mounted && Navigator.canPop(context)) { Navigator.pop(context); // Go back to the login page } } } } @override void dispose() { _emailController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: const Text('Reset Password'), leading: IconButton( // Custom back button if you want, or let default work icon: const Icon(Icons.arrow_back), onPressed: () => Navigator.of(context).pop(), ), ), body: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(25.0), child: Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ Icon( Icons.lock_reset_outlined, size: 80, color: theme.primaryColor, ), const SizedBox(height: 20), Text( 'Forgot Your Password?', textAlign: TextAlign.center, style: theme.textTheme.headlineSmall?.copyWith( color: theme.primaryColor, ), ), const SizedBox(height: 10), Text( 'Enter your email address below and we\'ll (mock) send you a link to reset your password.', textAlign: TextAlign.center, style: theme.textTheme.bodyLarge, ), const SizedBox(height: 40), TextFormField( controller: _emailController, decoration: const InputDecoration( hintText: 'Enter your Email Address', prefixIcon: Icon(Icons.email_outlined), ), keyboardType: TextInputType.emailAddress, validator: _validateEmail, autovalidateMode: AutovalidateMode.onUserInteraction, ), const SizedBox(height: 30), _isLoading ? const Center(child: CircularProgressIndicator()) : ElevatedButton( onPressed: _sendResetLink, child: const Text('Send Reset Link'), ), const SizedBox(height: 20), TextButton( onPressed: () { if (Navigator.canPop(context)) { Navigator.pop(context); // Go back to login } else { // Fallback if it can't pop (e.g., if opened directly) Navigator.pushReplacementNamed(context, '/'); } }, child: Text( 'Back to Login', style: TextStyle(color: theme.primaryColor), ), ) ], ), ), ), ), ); } }
|
|
<end of forgot_password_page.dart>
|
|
|
|
<start of home_page.dart>
|
|
import 'package:flutter/material.dart'; import '../data/mock_products.dart'; import '../models/product_model.dart'; import '../widgets/product_card.dart'; import '../main.dart'; // Import main.dart to access myAppKey class HomePage extends StatefulWidget { const HomePage({super.key}); @override State<HomePage> createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { late List<Product> _filteredProducts; String _searchQuery = ''; final TextEditingController _searchController = TextEditingController(); // ... (initState, dispose, _performSearch, _clearSearch methods remain the same) ... @override void initState() { super.initState(); _filteredProducts = List.from(mockProducts); _searchController.addListener(() { _performSearch(); }); } @override void dispose() { _searchController.dispose(); super.dispose(); } void _performSearch() { final query = _searchController.text.toLowerCase().trim(); setState(() { _searchQuery = query; if (_searchQuery.isEmpty) { _filteredProducts = List.from(mockProducts); } else { _filteredProducts = mockProducts.where((product) { final productName = product.name.toLowerCase(); final productDescription = product.description.toLowerCase(); return productName.contains(_searchQuery) || productDescription.contains(_searchQuery); }).toList(); } }); } void _clearSearch() { _searchController.clear(); } @override Widget build(BuildContext context) { final double screenWidth = MediaQuery.of(context).size.width; int crossAxisCount = 2; if (screenWidth > 1200) { crossAxisCount = 4; } else if (screenWidth > 800) { crossAxisCount = 3; } // Access the public themeMode final currentThemeMode = myAppKey.currentState?.themeMode ?? ThemeMode.light; final isDarkMode = currentThemeMode == ThemeMode.dark; return Scaffold( appBar: AppBar( title: const Text('Amazons - Browse Gadgets'), automaticallyImplyLeading: false, centerTitle: true, actions: [ Tooltip( message: isDarkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode', child: Switch( value: isDarkMode, onChanged: (value) { myAppKey.currentState?.changeTheme( value ? ThemeMode.dark : ThemeMode.light, ); }, activeColor: Theme.of(context).colorScheme.primary, inactiveThumbColor: Colors.grey[100], ), ), IconButton( icon: const Icon(Icons.notification_add_outlined), tooltip: 'Notifications', onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Wi-Fi status: Connected (mock)')), ); }, ), IconButton( icon: const Icon(Icons.logout), tooltip: 'Logout', onPressed: () { Navigator.pushNamedAndRemoveUntil(context, '/', (route) => false); }, ), ], ), // ... (rest of your HomePage body remains the same) .. body: Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0), child: TextField( controller: _searchController, decoration: InputDecoration( hintText: 'Search gadgets by name or description...', prefixIcon: const Icon(Icons.search), suffixIcon: _searchQuery.isNotEmpty ? IconButton( icon: const Icon(Icons.clear), onPressed: _clearSearch, ) : null, ), ), ), if (_filteredProducts.isEmpty && _searchQuery.isNotEmpty) Expanded( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.search_off, size: 60, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'No gadgets found for "$_searchQuery"', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: Colors.grey[600]), textAlign: TextAlign.center, ), const SizedBox(height: 8), ElevatedButton( onPressed: _clearSearch, child: const Text('Clear Search'), ) ], ), ), ) else Expanded( child: GridView.builder( padding: const EdgeInsets.fromLTRB(10.0, 0, 10.0, 10.0), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 0.54, ), itemCount: _filteredProducts.length, itemBuilder: (BuildContext context, int index) { return ProductCard(product: _filteredProducts[index]); }, ), ), ], ), ); } }
|
|
<end of home_page.dart>
|
|
|
|
<start of login_signup_page.dart>
|
|
import 'package:flutter/material.dart'; class LoginSignupPage extends StatefulWidget { const LoginSignupPage({super.key}); @override State<LoginSignupPage> createState() => _LoginSignupPageState(); } class _LoginSignupPageState extends State<LoginSignupPage> { final _formKey = GlobalKey<FormState>(); final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); final TextEditingController _phoneController = TextEditingController(); String? _errorMessage; bool _isLoading = false; bool _isLoginMode = true; // ... (rest of your validation methods, _toggleMode, _submitForm, dispose) ... // No changes needed in the methods above for this step @override void dispose() { _emailController.dispose(); _passwordController.dispose(); _phoneController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return Scaffold( appBar: AppBar( title: Text(_isLoginMode ? 'Amazons Login' : 'Amazons Sign Up'), centerTitle: true, actions: [ IconButton( icon: const Icon(Icons.wifi), onPressed: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Wi-Fi status: Connected (mock)')), ); }, ), ], ), body: Center( child: SingleChildScrollView( padding: const EdgeInsets.all(25.0), child: Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ // --- YOUR IMAGE LOGO --- Image.asset( 'images/logo.png', // <-- REPLACE 'your_logo.png' WITH YOUR ACTUAL FILENAME height: 80, // Adjust height as needed width: 150, // You can also set width fit: BoxFit.contain, // How the image should be inscribed into the space ), const SizedBox(height: 20), // Spacing after logo Text( _isLoginMode ? 'Welcome Back!' : 'Create Account', textAlign: TextAlign.center, style: theme.textTheme.displayLarge?.copyWith( color: theme.primaryColor, ), ), const SizedBox(height: 10), Text( _isLoginMode ? 'Login to continue' : 'Fill in the details to join', textAlign: TextAlign.center, style: theme.textTheme.titleMedium, ), const SizedBox(height: 40), // ... (rest of your form fields and buttons) ... // No changes needed below for this step // --- Email Text Field --- TextFormField( controller: _emailController, decoration: const InputDecoration( hintText: 'Email Address', prefixIcon: Icon(Icons.email_outlined), ), keyboardType: TextInputType.emailAddress, validator: _validateEmail, autovalidateMode: AutovalidateMode.onUserInteraction, ), const SizedBox(height: 20), // --- Password Text Field --- TextFormField( controller: _passwordController, decoration: const InputDecoration( hintText: 'Password', prefixIcon: Icon(Icons.lock_outline), ), obscureText: true, validator: _validatePassword, autovalidateMode: AutovalidateMode.onUserInteraction, ), const SizedBox(height: 20), // --- Phone Number Text Field (Conditional) --- if (!_isLoginMode) Padding( padding: const EdgeInsets.only(bottom: 20.0), child: TextFormField( controller: _phoneController, decoration: const InputDecoration( hintText: 'Phone Number', prefixIcon: Icon(Icons.phone_outlined), ), keyboardType: TextInputType.phone, validator: _validatePhoneNumber, autovalidateMode: AutovalidateMode.onUserInteraction, ), ), // --- Error Message Display --- if (_errorMessage != null && !_isLoading) Padding( padding: const EdgeInsets.only(bottom: 15.0), child: Text( _errorMessage!, style: TextStyle( color: theme.colorScheme.error, fontWeight: FontWeight.w500), textAlign: TextAlign.center, ), ), // --- Loading Indicator or Main Action Button --- _isLoading ? const Center(child: CircularProgressIndicator()) : ElevatedButton( onPressed: _submitForm, child: Text(_isLoginMode ? 'Login' : 'Create Account'), ), const SizedBox(height: 15), // --- Toggle Mode Button --- if (!_isLoading) TextButton( onPressed: _toggleMode, child: Text( _isLoginMode ? "Don't have an account? Sign Up" : "Already have an account? Login", style: TextStyle(color: theme.primaryColor), ), ), const SizedBox(height: 10), // --- Forgot Password (Only in Login Mode) --- if (_isLoginMode && !_isLoading) Padding( // Added padding for better spacing padding: const EdgeInsets.only(top: 8.0), child: TextButton( onPressed: (){ // Navigate to the Forgot Password page Navigator.pushNamed(context, '/forgot-password'); }, child: Text( 'Forgot Password?', style: TextStyle(color: theme.primaryColor), ) ), ) ], ), ), ), ), ); } // Paste your existing _validateEmail, _validatePassword, _validatePhoneNumber, // _toggleMode, and _submitForm methods here if they were not fully included above. String? _validateEmail(String? value) { if (value == null || value.isEmpty) { return 'Please enter your email'; } if (!RegExp(r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+").hasMatch(value)) { return 'Please enter a valid email'; } return null; } String? _validatePassword(String? value) { if (value == null || value.isEmpty) { return 'Please enter your password'; } if (value.length < 6) { return 'Password must be at least 6 characters'; } return null; } String? _validatePhoneNumber(String? value) { // This validator is only active when the field is present (i.e., signup mode) if (value == null || value.isEmpty) { return 'Please enter your phone number'; } if (value.length < 10) { // Basic length check return 'Phone number must be at least 10 digits'; } if (!RegExp(r'^[0-9]+$').hasMatch(value)) { // Digits only return 'Please enter a valid phone number (digits only)'; } return null; } void _toggleMode() { setState(() { _isLoginMode = !_isLoginMode; _errorMessage = null; // Clear errors when switching modes _formKey.currentState?.reset(); // Optional: reset form fields _emailController.clear(); _passwordController.clear(); _phoneController.clear(); }); } Future<void> _submitForm() async { setState(() { _errorMessage = null; }); if (_formKey.currentState!.validate()) { setState(() { _isLoading = true; }); await Future.delayed(const Duration(seconds: 1)); // Simulate network String password = _passwordController.text; String email = _emailController.text; String actionType = _isLoginMode ? "Login" : "Sign Up"; if (password.startsWith('gym')) { print('$actionType successful for: $email'); if (!_isLoginMode) { print('Phone Number: ${_phoneController.text}'); } if (mounted) { // Check if the widget is still in the tree Navigator.pushReplacementNamed(context, '/home'); } } else { setState(() { _errorMessage = 'Password must start with "gym" to $actionType.'; }); } setState(() { _isLoading = false; }); } } }
|
|
<end of login_signup_page.dart>
|
|
|
|
<start of main.dart>
|
|
import 'package:flutter/material.dart'; import 'login_signup_page.dart'; import 'home_page.dart'; import 'forgot_password_page.dart'; import 'theme/app_theme.dart'; final GlobalKey<_MyAppState> myAppKey = GlobalKey(); void main() { runApp(MyApp(key: myAppKey)); } class MyApp extends StatefulWidget { const MyApp({super.key}); @override State<MyApp> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { ThemeMode themeMode = ThemeMode.light; // Renamed from _themeMode (now public) void changeTheme(ThemeMode newThemeMode) { // Parameter renamed for clarity setState(() { themeMode = newThemeMode; }); } @override Widget build(BuildContext context) { return MaterialApp( title: 'Amazons Mock App', debugShowCheckedModeBanner: false, theme: AppTheme.lightTheme, darkTheme: AppTheme.darkTheme, themeMode: themeMode, // Use the public themeMode initialRoute: '/', routes: { '/': (context) => const LoginSignupPage(), '/home': (context) => const HomePage(), '/forgot-password': (context) => const ForgotPasswordPage(), }, ); } }
|
|
<end of main.dart>
|
|
|
|
<start of models/product_model.dart>
|
|
class Product { final String id; final String name; final String description; final String imageUrl; // We'll use placeholder URLs final double price; final String category; Product({ required this.id, required this.name, required this.description, required this.imageUrl, required this.price, required this.category, }); }
|
|
<end of models/product_model.dart>
|
|
|
|
<start of theme/app_theme.dart>
|
|
import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; /// Enhanced Flutter theme with improved light and dark modes /// Features better color cohesion, refined component styles, and additional theme elements // --- Core Color Palettes --- class AppColors { // Light Theme static const primaryLight = Color(0xFFFF9900); // Amazon Orange static const secondaryLight = Color(0xFF232F3E); // Amazon Dark Blue static const accentLight = Color(0xFF037EC0); // Complementary Blue static const errorLight = Color(0xFFD32F2F); // Error Red static const successLight = Color(0xFF388E3C); // Success Green static const warningLight = Color(0xFFFFA000); // Warning Amber static const infoLight = Color(0xFF1976D2); // Info Blue // Backgrounds & Surfaces (Light) static const scaffoldLight = Color(0xFFFAFAFA); // Off-white for less eye strain static const cardLight = Colors.white; static const textFieldLight = Color(0xFFF5F5F5); static const dividerLight = Color(0xFFE0E0E0); // Text Colors (Light) static const textPrimaryLight = Color(0xFF212121); static const textSecondaryLight = Color(0xFF757575); static const textTertiaryLight = Color(0xFF9E9E9E); static const onPrimaryLight = Colors.white; // Dark Theme static const primaryDark = Color(0xFFFFAB40); // Lighter Orange for dark theme static const secondaryDark = Color(0xFF1A2433); // Deeper blue for dark mode static const accentDark = Color(0xFF4FC3F7); // Light Blue accent static const errorDark = Color(0xFFEF5350); // Brighter Error static const successDark = Color(0xFF66BB6A); // Brighter Success static const warningDark = Color(0xFFFFD54F); // Brighter Warning static const infoDark = Color(0xFF42A5F5); // Brighter Info // Backgrounds & Surfaces (Dark) static const scaffoldDark = Color(0xFF121212); // Material dark background static const cardDark = Color(0xFF1D1D1D); // Slightly lighter than scaffold static const textFieldDark = Color(0xFF2C2C2C); // Lighter input fields for contrast static const dividerDark = Color(0xFF424242); // Dark dividers // Text Colors (Dark) static const textPrimaryDark = Color(0xFFECECEC); // Not pure white for less eye strain static const textSecondaryDark = Color(0xFFB0B0B0); static const textTertiaryDark = Color(0xFF787878); static const onPrimaryDark = Color(0xFF121212); // Dark text on light buttons } // Common spacing & dimensions class AppDimensions { static const double xs = 4.0; static const double sm = 8.0; static const double md = 16.0; static const double lg = 24.0; static const double xl = 32.0; static const double buttonHeight = 52.0; static const double buttonRadius = 8.0; static const double cardRadius = 12.0; static const double textFieldRadius = 8.0; } class AppTheme { static TextTheme _buildTextTheme(TextTheme base, Color primaryTextColor, Color secondaryTextColor) { return base.copyWith( displayLarge: GoogleFonts.poppins( fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: -0.5, color: primaryTextColor, ), displayMedium: GoogleFonts.poppins( fontSize: 24, fontWeight: FontWeight.w700, letterSpacing: -0.25, color: primaryTextColor, ), displaySmall: GoogleFonts.poppins( fontSize: 20, fontWeight: FontWeight.w700, color: primaryTextColor, ), headlineMedium: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.w600, color: primaryTextColor, ), headlineSmall: GoogleFonts.poppins( fontSize: 16, fontWeight: FontWeight.w600, color: primaryTextColor, ), titleLarge: GoogleFonts.poppins( fontSize: 16, fontWeight: FontWeight.w600, color: primaryTextColor, ), titleMedium: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w500, color: primaryTextColor, ), titleSmall: GoogleFonts.poppins( fontSize: 13, fontWeight: FontWeight.w500, color: secondaryTextColor, ), bodyLarge: GoogleFonts.poppins( fontSize: 16, color: primaryTextColor, ), bodyMedium: GoogleFonts.poppins( fontSize: 14, color: primaryTextColor, ), bodySmall: GoogleFonts.poppins( fontSize: 12, color: secondaryTextColor, ), labelLarge: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w600, ), ); } // Shared button style builder static ButtonStyle _buildButtonStyle({ required Color backgroundColor, required Color foregroundColor, Color? borderColor, double height = AppDimensions.buttonHeight, }) { return ButtonStyle( backgroundColor: MaterialStateProperty.resolveWith<Color>((states) { if (states.contains(MaterialState.disabled)) { return backgroundColor.withOpacity(0.3); } if (states.contains(MaterialState.pressed)) { return backgroundColor.withOpacity(0.8); } return backgroundColor; }), foregroundColor: MaterialStateProperty.resolveWith<Color>((states) { if (states.contains(MaterialState.disabled)) { return foregroundColor.withOpacity(0.5); } return foregroundColor; }), overlayColor: MaterialStateProperty.resolveWith<Color>((states) { return foregroundColor.withOpacity(0.1); }), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppDimensions.buttonRadius), side: borderColor != null ? BorderSide(color: borderColor, width: 1.5) : BorderSide.none, ), ), minimumSize: MaterialStateProperty.all<Size>(Size(double.infinity, height)), padding: MaterialStateProperty.all<EdgeInsets>( const EdgeInsets.symmetric(horizontal: AppDimensions.lg), ), elevation: MaterialStateProperty.resolveWith<double>((states) { if (states.contains(MaterialState.disabled)) return 0; if (states.contains(MaterialState.pressed)) return 1; return 2; }), ); } static ThemeData get lightTheme { final base = ThemeData.light(); final textTheme = _buildTextTheme( base.textTheme, AppColors.textPrimaryLight, AppColors.textSecondaryLight, ); return ThemeData( brightness: Brightness.light, primaryColor: AppColors.primaryLight, primaryColorDark: AppColors.primaryLight.withOpacity(0.8), primaryColorLight: AppColors.primaryLight.withOpacity(0.4), canvasColor: AppColors.scaffoldLight, scaffoldBackgroundColor: AppColors.scaffoldLight, cardColor: AppColors.cardLight, dividerColor: AppColors.dividerLight, focusColor: AppColors.primaryLight.withOpacity(0.12), hoverColor: AppColors.primaryLight.withOpacity(0.06), splashColor: AppColors.primaryLight.withOpacity(0.15), fontFamily: GoogleFonts.poppins().fontFamily, // AppBar appBarTheme: AppBarTheme( backgroundColor: AppColors.primaryLight, elevation: 0, centerTitle: false, iconTheme: const IconThemeData(color: AppColors.onPrimaryLight), titleTextStyle: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.onPrimaryLight, ), toolbarHeight: 56, shadowColor: AppColors.secondaryLight.withOpacity(0.15), ), // Buttons elevatedButtonTheme: ElevatedButtonThemeData( style: _buildButtonStyle( backgroundColor: AppColors.primaryLight, foregroundColor: AppColors.onPrimaryLight, ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: _buildButtonStyle( backgroundColor: Colors.transparent, foregroundColor: AppColors.primaryLight, borderColor: AppColors.primaryLight, ), ), textButtonTheme: TextButtonThemeData( style: ButtonStyle( foregroundColor: MaterialStateProperty.all<Color>(AppColors.primaryLight), textStyle: MaterialStateProperty.all<TextStyle>( GoogleFonts.poppins(fontWeight: FontWeight.w600), ), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppDimensions.sm)), ), overlayColor: MaterialStateProperty.all<Color>( AppColors.primaryLight.withOpacity(0.1), ), ), ), // Card cardTheme: CardTheme( color: AppColors.cardLight, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppDimensions.cardRadius), ), shadowColor: AppColors.secondaryLight.withOpacity(0.1), ), // Inputs inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: AppColors.textFieldLight, contentPadding: const EdgeInsets.symmetric( horizontal: AppDimensions.md, vertical: AppDimensions.md, ), hintStyle: GoogleFonts.poppins( color: AppColors.textTertiaryLight, fontSize: 14, ), labelStyle: GoogleFonts.poppins( color: AppColors.textSecondaryLight, fontSize: 14, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppDimensions.textFieldRadius), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppDimensions.textFieldRadius), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppDimensions.textFieldRadius), borderSide: const BorderSide(color: AppColors.primaryLight, width: 1.5), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppDimensions.textFieldRadius), borderSide: const BorderSide(color: AppColors.errorLight, width: 1.5), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppDimensions.textFieldRadius), borderSide: const BorderSide(color: AppColors.errorLight, width: 1.5), ), prefixIconColor: AppColors.textSecondaryLight, suffixIconColor: AppColors.textSecondaryLight, ), // Checkbox & Toggle checkboxTheme: CheckboxThemeData( fillColor: MaterialStateProperty.resolveWith<Color>((states) { if (states.contains(MaterialState.disabled)) { return AppColors.textTertiaryLight; } if (states.contains(MaterialState.selected)) { return AppColors.primaryLight; } return Colors.transparent; }), side: const BorderSide(width: 1.5, color: AppColors.textSecondaryLight), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3)), ), switchTheme: SwitchThemeData( thumbColor: MaterialStateProperty.resolveWith<Color>((states) { if (states.contains(MaterialState.disabled)) { return AppColors.textTertiaryLight; } if (states.contains(MaterialState.selected)) { return AppColors.primaryLight; } return Colors.white; }), trackColor: MaterialStateProperty.resolveWith<Color>((states) { if (states.contains(MaterialState.disabled)) { return AppColors.dividerLight; } if (states.contains(MaterialState.selected)) { return AppColors.primaryLight.withOpacity(0.4); } return AppColors.textTertiaryLight; }), ), // Chip chipTheme: ChipThemeData( backgroundColor: AppColors.primaryLight.withOpacity(0.08), disabledColor: AppColors.dividerLight, selectedColor: AppColors.primaryLight.withOpacity(0.2), labelStyle: GoogleFonts.poppins( fontSize: 12, color: AppColors.textPrimaryLight, ), secondaryLabelStyle: GoogleFonts.poppins( fontSize: 12, color: AppColors.primaryLight, fontWeight: FontWeight.w500, ), padding: const EdgeInsets.symmetric( horizontal: AppDimensions.md, vertical: AppDimensions.xs, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppDimensions.sm), ), ), // Dialogs & Bottom Sheets dialogTheme: DialogTheme( backgroundColor: AppColors.cardLight, elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppDimensions.lg), ), ), bottomSheetTheme: const BottomSheetThemeData( backgroundColor: AppColors.cardLight, elevation: 8, shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(AppDimensions.xl), topRight: Radius.circular(AppDimensions.xl), ), ), ), // Progress Indicators progressIndicatorTheme: const ProgressIndicatorThemeData( color: AppColors.primaryLight, linearTrackColor: AppColors.dividerLight, refreshBackgroundColor: AppColors.dividerLight, ), // Color Scheme colorScheme: ColorScheme.light( primary: AppColors.primaryLight, primaryContainer: AppColors.primaryLight.withOpacity(0.15), onPrimaryContainer: AppColors.primaryLight.withOpacity(0.8), secondary: AppColors.secondaryLight, secondaryContainer: AppColors.secondaryLight.withOpacity(0.1), onSecondaryContainer: AppColors.secondaryLight, tertiary: AppColors.accentLight, tertiaryContainer: AppColors.accentLight.withOpacity(0.1), onTertiaryContainer: AppColors.accentLight, error: AppColors.errorLight, errorContainer: AppColors.errorLight.withOpacity(0.1), onErrorContainer: AppColors.errorLight, surface: AppColors.cardLight, background: AppColors.scaffoldLight, onBackground: AppColors.textPrimaryLight, onSurface: AppColors.textPrimaryLight, onPrimary: AppColors.onPrimaryLight, onSecondary: Colors.white, onError: Colors.white, ), textTheme: textTheme, ); } static ThemeData get darkTheme { final base = ThemeData.dark(); final textTheme = _buildTextTheme( base.textTheme, AppColors.textPrimaryDark, AppColors.textSecondaryDark, ); return ThemeData( brightness: Brightness.dark, primaryColor: AppColors.primaryDark, primaryColorDark: AppColors.primaryDark.withOpacity(0.8), primaryColorLight: AppColors.primaryDark.withOpacity(0.4), canvasColor: AppColors.scaffoldDark, scaffoldBackgroundColor: AppColors.scaffoldDark, cardColor: AppColors.cardDark, dividerColor: AppColors.dividerDark, focusColor: AppColors.primaryDark.withOpacity(0.12), hoverColor: AppColors.primaryDark.withOpacity(0.06), splashColor: AppColors.primaryDark.withOpacity(0.15), fontFamily: GoogleFonts.poppins().fontFamily, // AppBar appBarTheme: AppBarTheme( backgroundColor: AppColors.secondaryDark, elevation: 0, centerTitle: false, iconTheme: const IconThemeData(color: AppColors.textPrimaryDark), titleTextStyle: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.textPrimaryDark, ), toolbarHeight: 56, shadowColor: Colors.black.withOpacity(0.2), ), // Buttons elevatedButtonTheme: ElevatedButtonThemeData( style: _buildButtonStyle( backgroundColor: AppColors.primaryDark, foregroundColor: AppColors.onPrimaryDark, ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: _buildButtonStyle( backgroundColor: Colors.transparent, foregroundColor: AppColors.primaryDark, borderColor: AppColors.primaryDark, ), ), textButtonTheme: TextButtonThemeData( style: ButtonStyle( foregroundColor: MaterialStateProperty.all<Color>(AppColors.primaryDark), textStyle: MaterialStateProperty.all<TextStyle>( GoogleFonts.poppins(fontWeight: FontWeight.w600), ), shape: MaterialStateProperty.all<RoundedRectangleBorder>( RoundedRectangleBorder(borderRadius: BorderRadius.circular(AppDimensions.sm)), ), overlayColor: MaterialStateProperty.all<Color>( AppColors.primaryDark.withOpacity(0.15), ), ), ), // Card cardTheme: CardTheme( color: AppColors.cardDark, elevation: 2, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppDimensions.cardRadius), ), shadowColor: Colors.black.withOpacity(0.3), ), // Inputs inputDecorationTheme: InputDecorationTheme( filled: true, fillColor: AppColors.textFieldDark, contentPadding: const EdgeInsets.symmetric( horizontal: AppDimensions.md, vertical: AppDimensions.md, ), hintStyle: GoogleFonts.poppins( color: AppColors.textTertiaryDark, fontSize: 14, ), labelStyle: GoogleFonts.poppins( color: AppColors.textSecondaryDark, fontSize: 14, ), border: OutlineInputBorder( borderRadius: BorderRadius.circular(AppDimensions.textFieldRadius), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppDimensions.textFieldRadius), borderSide: BorderSide.none, ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppDimensions.textFieldRadius), borderSide: const BorderSide(color: AppColors.primaryDark, width: 1.5), ), errorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppDimensions.textFieldRadius), borderSide: const BorderSide(color: AppColors.errorDark, width: 1.5), ), focusedErrorBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(AppDimensions.textFieldRadius), borderSide: const BorderSide(color: AppColors.errorDark, width: 1.5), ), prefixIconColor: AppColors.textSecondaryDark, suffixIconColor: AppColors.textSecondaryDark, ), // Checkbox & Toggle checkboxTheme: CheckboxThemeData( fillColor: MaterialStateProperty.resolveWith<Color>((states) { if (states.contains(MaterialState.disabled)) { return AppColors.textTertiaryDark; } if (states.contains(MaterialState.selected)) { return AppColors.primaryDark; } return Colors.transparent; }), side: const BorderSide(width: 1.5, color: AppColors.textSecondaryDark), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(3)), ), switchTheme: SwitchThemeData( thumbColor: MaterialStateProperty.resolveWith<Color>((states) { if (states.contains(MaterialState.disabled)) { return AppColors.textTertiaryDark; } if (states.contains(MaterialState.selected)) { return AppColors.primaryDark; } return Colors.white; }), trackColor: MaterialStateProperty.resolveWith<Color>((states) { if (states.contains(MaterialState.disabled)) { return AppColors.dividerDark; } if (states.contains(MaterialState.selected)) { return AppColors.primaryDark.withOpacity(0.4); } return AppColors.textTertiaryDark; }), ), // Chip chipTheme: ChipThemeData( backgroundColor: AppColors.primaryDark.withOpacity(0.12), disabledColor: AppColors.dividerDark, selectedColor: AppColors.primaryDark.withOpacity(0.25), labelStyle: GoogleFonts.poppins( fontSize: 12, color: AppColors.textPrimaryDark, ), secondaryLabelStyle: GoogleFonts.poppins( fontSize: 12, color: AppColors.primaryDark, fontWeight: FontWeight.w500, ), padding: const EdgeInsets.symmetric( horizontal: AppDimensions.md, vertical: AppDimensions.xs, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppDimensions.sm), ), ), // Dialogs & Bottom Sheets dialogTheme: DialogTheme( backgroundColor: AppColors.cardDark, elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(AppDimensions.lg), ), ), bottomSheetTheme: const BottomSheetThemeData( backgroundColor: AppColors.cardDark, elevation: 8, shape: RoundedRectangleBorder( borderRadius: BorderRadius.only( topLeft: Radius.circular(AppDimensions.xl), topRight: Radius.circular(AppDimensions.xl), ), ), ), // Progress Indicators progressIndicatorTheme: const ProgressIndicatorThemeData( color: AppColors.primaryDark, linearTrackColor: AppColors.dividerDark, refreshBackgroundColor: AppColors.dividerDark, ), // Color Scheme colorScheme: ColorScheme.dark( primary: AppColors.primaryDark, primaryContainer: AppColors.primaryDark.withOpacity(0.15), onPrimaryContainer: AppColors.primaryDark, secondary: AppColors.secondaryDark, secondaryContainer: AppColors.secondaryDark.withOpacity(0.1), onSecondaryContainer: AppColors.textPrimaryDark, tertiary: AppColors.accentDark, tertiaryContainer: AppColors.accentDark.withOpacity(0.1), onTertiaryContainer: AppColors.accentDark, error: AppColors.errorDark, errorContainer: AppColors.errorDark.withOpacity(0.1), onErrorContainer: AppColors.errorDark, surface: AppColors.cardDark, background: AppColors.scaffoldDark, onBackground: AppColors.textPrimaryDark, onSurface: AppColors.textPrimaryDark, onPrimary: AppColors.onPrimaryDark, onSecondary: AppColors.textPrimaryDark, onError: Colors.black, ), textTheme: textTheme, ); } }
|
|
<end of theme/app_theme.dart>
|
|
|
|
<start of widgets/product_card.dart>
|
|
import 'package:flutter/material.dart'; import '../models/product_model.dart'; /// An improved ProductCard widget that works with the enhanced theme system /// - More efficient layout with better organization /// - Uses semantic theme properties for consistent styling /// - Responsive design with flexible sizing class ProductCard extends StatelessWidget { final Product product; final VoidCallback? onAddToCart; const ProductCard({ super.key, required this.product, this.onAddToCart, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return Card( clipBehavior: Clip.antiAlias, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ // Product Image _buildProductImage(context), // Product Details Expanded( child: Padding( padding: const EdgeInsets.all(12.0), child: _buildProductDetails(context), ), ), ], ), ); } Widget _buildProductImage(BuildContext context) { return AspectRatio( aspectRatio: 3/2, child: Stack( fit: StackFit.expand, children: [ // Image with loading and error handling Image.network( product.imageUrl, fit: BoxFit.cover, loadingBuilder: _imageLoadingBuilder, errorBuilder: _imageErrorBuilder, ), // Category badge overlay Positioned( top: 8, right: 8, child: _buildCategoryBadge(context), ), ], ), ); } Widget _buildCategoryBadge(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: colorScheme.secondaryContainer.withOpacity(0.85), borderRadius: BorderRadius.circular(4), ), child: Text( product.category, style: theme.textTheme.labelSmall?.copyWith( color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w500, ), ), ); } Widget _buildProductDetails(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Product Name Text( product.name, style: Theme.of(context).textTheme.titleMedium, maxLines: 1, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), // Product Description Text( product.description, style: Theme.of(context).textTheme.bodySmall, maxLines: 2, overflow: TextOverflow.ellipsis, ), const Spacer(), // Price and Add to Cart _buildPriceAndAction(context), ], ); } Widget _buildPriceAndAction(BuildContext context) { final theme = Theme.of(context); return Row( children: [ // Price display Text( '\$${product.price.toStringAsFixed(2)}', style: theme.textTheme.titleMedium?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.bold, ), ), const Spacer(), // Add to cart button _buildAddToCartButton(context), ], ); } Widget _buildAddToCartButton(BuildContext context) { return IconButton( onPressed: () => _handleAddToCart(context), icon: const Icon(Icons.add_shopping_cart_outlined), style: IconButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.primary.withOpacity(0.1), foregroundColor: Theme.of(context).colorScheme.primary, ), tooltip: 'Add to cart', ); } void _handleAddToCart(BuildContext context) { if (onAddToCart != null) { onAddToCart!(); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('${product.name} added to cart')), ); } } Widget _imageLoadingBuilder(BuildContext context, Widget child, ImageChunkEvent? loadingProgress) { if (loadingProgress == null) return child; return Center( child: CircularProgressIndicator( value: loadingProgress.expectedTotalBytes != null ? loadingProgress.cumulativeBytesLoaded / loadingProgress.expectedTotalBytes! : null, strokeWidth: 2.0, color: Theme.of(context).colorScheme.primary, ), ); } Widget _imageErrorBuilder(BuildContext context, Object exception, StackTrace? stackTrace) { final theme = Theme.of(context); return Container( color: theme.colorScheme.surfaceVariant, child: Center( child: Icon( Icons.broken_image_outlined, size: 40, color: theme.colorScheme.onSurfaceVariant.withOpacity(0.6), ), ), ); } }
|
|
<end of widgets/product_card.dart> |