This commit is contained in:
Phuoc Nguyen
2025-10-17 17:22:28 +07:00
parent 2125e85d40
commit 628c81ce13
86 changed files with 31339 additions and 1710 deletions

View 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',
),
],
);
}
}

View 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,
),
);
}
}

View 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,
),
),
),
],
],
),
),
);
}
}

View 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),
),
),
),
],
],
),
),
);
}
}

View 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,
),
),
),
),
],
),
);
}
}

View 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,
),
],
],
),
);
}
}