runable
This commit is contained in:
84
lib/core/widgets/bottom_nav_bar.dart
Normal file
84
lib/core/widgets/bottom_nav_bar.dart
Normal file
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Custom bottom navigation bar for the Worker app.
|
||||
///
|
||||
/// This widget will be fully implemented once navigation system is in place.
|
||||
/// It will support 5 main tabs: Home, Products, Loyalty, Account, and More.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// CustomBottomNavBar(
|
||||
/// currentIndex: _currentIndex,
|
||||
/// onTap: (index) => _onNavigate(index),
|
||||
/// )
|
||||
/// ```
|
||||
class CustomBottomNavBar extends StatelessWidget {
|
||||
/// Current selected tab index
|
||||
final int currentIndex;
|
||||
|
||||
/// Callback when a tab is tapped
|
||||
final ValueChanged<int> onTap;
|
||||
|
||||
/// Optional badge count for notifications
|
||||
final int? badgeCount;
|
||||
|
||||
const CustomBottomNavBar({
|
||||
super.key,
|
||||
required this.currentIndex,
|
||||
required this.onTap,
|
||||
this.badgeCount,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Will be implemented with navigation
|
||||
// TODO: Implement full bottom navigation with:
|
||||
// - Home tab (home icon)
|
||||
// - Products tab (shopping_bag icon)
|
||||
// - Loyalty tab (card_membership icon)
|
||||
// - Account tab (person icon)
|
||||
// - More tab (menu icon) with notification badge
|
||||
//
|
||||
// Design specs:
|
||||
// - Height: 72px
|
||||
// - Icon size: 24px (selected: 28px)
|
||||
// - Label font size: 12px
|
||||
// - Selected color: primaryBlue
|
||||
// - Unselected color: grey500
|
||||
// - Badge: red circle with white text
|
||||
|
||||
return BottomNavigationBar(
|
||||
currentIndex: currentIndex,
|
||||
onTap: onTap,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
selectedItemColor: AppColors.primaryBlue,
|
||||
unselectedItemColor: AppColors.grey500,
|
||||
selectedFontSize: 12,
|
||||
unselectedFontSize: 12,
|
||||
items: const [
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home),
|
||||
label: 'Home',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.shopping_bag),
|
||||
label: 'Products',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.card_membership),
|
||||
label: 'Loyalty',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.person),
|
||||
label: 'Account',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.menu),
|
||||
label: 'More',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
144
lib/core/widgets/custom_button.dart
Normal file
144
lib/core/widgets/custom_button.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Button variant types for different use cases.
|
||||
enum ButtonVariant {
|
||||
/// Primary button with filled background color
|
||||
primary,
|
||||
|
||||
/// Secondary button with outlined border
|
||||
secondary,
|
||||
}
|
||||
|
||||
/// Custom button widget following the Worker app design system.
|
||||
///
|
||||
/// Supports primary and secondary variants, loading states, and disabled states.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// CustomButton(
|
||||
/// text: 'Login',
|
||||
/// onPressed: () => _handleLogin(),
|
||||
/// variant: ButtonVariant.primary,
|
||||
/// isLoading: _isLoading,
|
||||
/// )
|
||||
/// ```
|
||||
class CustomButton extends StatelessWidget {
|
||||
/// The text to display on the button
|
||||
final String text;
|
||||
|
||||
/// Callback when button is pressed. If null, button is disabled.
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// Visual variant of the button (primary or secondary)
|
||||
final ButtonVariant variant;
|
||||
|
||||
/// Whether to show loading indicator instead of text
|
||||
final bool isLoading;
|
||||
|
||||
/// Optional icon to display before the text
|
||||
final IconData? icon;
|
||||
|
||||
/// Custom width for the button. If null, uses parent constraints.
|
||||
final double? width;
|
||||
|
||||
/// Custom height for the button. Defaults to 48.
|
||||
final double? height;
|
||||
|
||||
const CustomButton({
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.onPressed,
|
||||
this.variant = ButtonVariant.primary,
|
||||
this.isLoading = false,
|
||||
this.icon,
|
||||
this.width,
|
||||
this.height,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDisabled = onPressed == null || isLoading;
|
||||
|
||||
if (variant == ButtonVariant.primary) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height ?? 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: isDisabled ? null : onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: AppColors.grey500,
|
||||
disabledForegroundColor: Colors.white70,
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: _buildContent(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
height: height ?? 48,
|
||||
child: OutlinedButton(
|
||||
onPressed: isDisabled ? null : onPressed,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryBlue,
|
||||
disabledForegroundColor: AppColors.grey500,
|
||||
side: BorderSide(
|
||||
color: isDisabled ? AppColors.grey500 : AppColors.primaryBlue,
|
||||
width: 1.5,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: _buildContent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the button content (text, icon, or loading indicator)
|
||||
Widget _buildContent() {
|
||||
if (isLoading) {
|
||||
return const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 20),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
112
lib/core/widgets/empty_state.dart
Normal file
112
lib/core/widgets/empty_state.dart
Normal file
@@ -0,0 +1,112 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Empty state widget for displaying when lists or collections are empty.
|
||||
///
|
||||
/// Shows an icon, title, subtitle, and optional action button to guide users
|
||||
/// when there's no content to display.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// EmptyState(
|
||||
/// icon: Icons.shopping_cart_outlined,
|
||||
/// title: 'Your cart is empty',
|
||||
/// subtitle: 'Add some products to get started',
|
||||
/// actionLabel: 'Browse Products',
|
||||
/// onAction: () => Navigator.pushNamed(context, '/products'),
|
||||
/// )
|
||||
/// ```
|
||||
class EmptyState extends StatelessWidget {
|
||||
/// Icon to display at the top
|
||||
final IconData icon;
|
||||
|
||||
/// Main title text
|
||||
final String title;
|
||||
|
||||
/// Optional subtitle/description text
|
||||
final String? subtitle;
|
||||
|
||||
/// Optional action button label. If null, no button is shown.
|
||||
final String? actionLabel;
|
||||
|
||||
/// Optional callback for action button
|
||||
final VoidCallback? onAction;
|
||||
|
||||
/// Size of the icon. Defaults to 80.
|
||||
final double iconSize;
|
||||
|
||||
const EmptyState({
|
||||
super.key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
this.subtitle,
|
||||
this.actionLabel,
|
||||
this.onAction,
|
||||
this.iconSize = 80,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(32.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
if (actionLabel != null && onAction != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton(
|
||||
onPressed: onAction,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
actionLabel!,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
85
lib/core/widgets/error_widget.dart
Normal file
85
lib/core/widgets/error_widget.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Custom error widget for displaying error states with retry functionality.
|
||||
///
|
||||
/// Shows an error icon, message, and optional retry button. Used throughout
|
||||
/// the app for error states in async operations.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// CustomErrorWidget(
|
||||
/// message: 'Failed to load products',
|
||||
/// onRetry: () => _loadProducts(),
|
||||
/// )
|
||||
/// ```
|
||||
class CustomErrorWidget extends StatelessWidget {
|
||||
/// Error message to display
|
||||
final String message;
|
||||
|
||||
/// Optional callback for retry button. If null, no button is shown.
|
||||
final VoidCallback? onRetry;
|
||||
|
||||
/// Optional icon to display. Defaults to error_outline.
|
||||
final IconData? icon;
|
||||
|
||||
/// Size of the error icon. Defaults to 64.
|
||||
final double iconSize;
|
||||
|
||||
const CustomErrorWidget({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.onRetry,
|
||||
this.icon,
|
||||
this.iconSize = 64,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
icon ?? Icons.error_outline,
|
||||
size: iconSize,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.grey900,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (onRetry != null) ...[
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onRetry,
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Retry'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
87
lib/core/widgets/floating_chat_button.dart
Normal file
87
lib/core/widgets/floating_chat_button.dart
Normal file
@@ -0,0 +1,87 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Floating action button for chat support access.
|
||||
///
|
||||
/// Positioned at bottom-right of the screen with accent cyan color.
|
||||
/// Opens chat support when tapped.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// Scaffold(
|
||||
/// floatingActionButton: ChatFloatingButton(
|
||||
/// onPressed: () => Navigator.pushNamed(context, '/chat'),
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
class ChatFloatingButton extends StatelessWidget {
|
||||
/// Callback when the button is pressed
|
||||
final VoidCallback onPressed;
|
||||
|
||||
/// Optional badge count for unread messages
|
||||
final int? unreadCount;
|
||||
|
||||
/// Size of the FAB. Defaults to 56.
|
||||
final double size;
|
||||
|
||||
const ChatFloatingButton({
|
||||
super.key,
|
||||
required this.onPressed,
|
||||
this.unreadCount,
|
||||
this.size = 56,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: Stack(
|
||||
children: [
|
||||
FloatingActionButton(
|
||||
onPressed: onPressed,
|
||||
backgroundColor: AppColors.accentCyan,
|
||||
elevation: 6,
|
||||
child: const Icon(
|
||||
Icons.chat_bubble_outline,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
if (unreadCount != null && unreadCount! > 0)
|
||||
Positioned(
|
||||
right: 0,
|
||||
top: 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.danger,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Colors.white,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 20,
|
||||
minHeight: 20,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
unreadCount! > 99 ? '99+' : unreadCount.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
64
lib/core/widgets/loading_indicator.dart
Normal file
64
lib/core/widgets/loading_indicator.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Custom loading indicator widget with optional message text.
|
||||
///
|
||||
/// Displays a centered circular progress indicator with an optional
|
||||
/// message below it. Used for loading states throughout the app.
|
||||
///
|
||||
/// Example usage:
|
||||
/// ```dart
|
||||
/// CustomLoadingIndicator(
|
||||
/// message: 'Loading products...',
|
||||
/// )
|
||||
/// ```
|
||||
class CustomLoadingIndicator extends StatelessWidget {
|
||||
/// Optional message to display below the loading indicator
|
||||
final String? message;
|
||||
|
||||
/// Size of the loading indicator. Defaults to 40.
|
||||
final double size;
|
||||
|
||||
/// Color of the loading indicator. Defaults to primaryBlue.
|
||||
final Color? color;
|
||||
|
||||
const CustomLoadingIndicator({
|
||||
super.key,
|
||||
this.message,
|
||||
this.size = 40,
|
||||
this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
color ?? AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (message != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
message!,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user