add auth, format

This commit is contained in:
Phuoc Nguyen
2025-11-07 11:52:06 +07:00
parent 24a8508fce
commit 3803bd26e0
173 changed files with 8505 additions and 7116 deletions

View File

@@ -0,0 +1,216 @@
/// File Upload Card Widget
///
/// Reusable widget for uploading image files with preview.
library;
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// File Upload Card
///
/// A reusable widget for uploading files with preview functionality.
/// Features:
/// - Dashed border upload area
/// - Camera/file icon
/// - Title and subtitle
/// - Image preview after selection
/// - Remove button
///
/// Usage:
/// ```dart
/// FileUploadCard(
/// file: selectedFile,
/// onTap: () => pickImage(),
/// onRemove: () => removeImage(),
/// icon: Icons.camera_alt,
/// title: 'Chụp ảnh hoặc chọn file',
/// subtitle: 'JPG, PNG tối đa 5MB',
/// )
/// ```
class FileUploadCard extends StatelessWidget {
/// Creates a file upload card
const FileUploadCard({
super.key,
required this.file,
required this.onTap,
required this.onRemove,
required this.icon,
required this.title,
required this.subtitle,
});
/// Selected file (null if not selected)
final File? file;
/// Callback when upload area is tapped
final VoidCallback onTap;
/// Callback to remove selected file
final VoidCallback onRemove;
/// Icon to display in upload area
final IconData icon;
/// Title text
final String title;
/// Subtitle text
final String subtitle;
/// Format file size in bytes to human-readable string
String _formatFileSize(int bytes) {
if (bytes == 0) return '0 B';
const suffixes = ['B', 'KB', 'MB', 'GB'];
final i = (bytes.bitLength - 1) ~/ 10;
final size = bytes / (1 << (i * 10));
return '${size.toStringAsFixed(2)} ${suffixes[i]}';
}
/// Get file name from path
String _getFileName(String path) {
return path.split('/').last;
}
@override
Widget build(BuildContext context) {
if (file != null) {
// Show preview with remove option
return _buildPreview(context);
} else {
// Show upload area
return _buildUploadArea(context);
}
}
/// Build upload area
Widget _buildUploadArea(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.lg),
child: Container(
decoration: BoxDecoration(
color: AppColors.white,
border: Border.all(
color: const Color(0xFFCBD5E1),
width: 2,
strokeAlign: BorderSide.strokeAlignInside,
),
borderRadius: BorderRadius.circular(AppRadius.lg),
),
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
children: [
// Icon
Icon(icon, size: 32, color: AppColors.grey500),
const SizedBox(height: AppSpacing.sm),
// Title
Text(
title,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: AppSpacing.xs),
// Subtitle
Text(
subtitle,
style: const TextStyle(fontSize: 12, color: AppColors.grey500),
),
],
),
),
);
}
/// Build preview with remove button
Widget _buildPreview(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: AppColors.white,
border: Border.all(color: AppColors.grey100, width: 1),
borderRadius: BorderRadius.circular(AppRadius.md),
),
padding: const EdgeInsets.all(AppSpacing.sm),
child: Row(
children: [
// Thumbnail
ClipRRect(
borderRadius: BorderRadius.circular(AppRadius.sm),
child: Image.file(
file!,
width: 50,
height: 50,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 50,
height: 50,
color: AppColors.grey100,
child: const Icon(
Icons.broken_image,
color: AppColors.grey500,
size: 24,
),
);
},
),
),
const SizedBox(width: AppSpacing.sm),
// File info
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_getFileName(file!.path),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 2),
FutureBuilder<int>(
future: file!.length(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(
_formatFileSize(snapshot.data!),
style: const TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
);
}
return const SizedBox.shrink();
},
),
],
),
),
const SizedBox(width: AppSpacing.xs),
// Remove button
IconButton(
icon: const Icon(Icons.close, color: AppColors.danger, size: 20),
onPressed: onRemove,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
splashRadius: 20,
),
],
),
);
}
}

View File

@@ -0,0 +1,133 @@
/// Phone Input Field Widget
///
/// Custom text field for Vietnamese phone number input.
/// Supports formats: 0xxx xxx xxx, +84xxx xxx xxx, 84xxx xxx xxx
library;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// Phone Input Field
///
/// A custom text field widget specifically designed for Vietnamese phone number input.
/// Features:
/// - Phone icon prefix
/// - Numeric keyboard
/// - Phone number formatting
/// - Vietnamese phone validation support
///
/// Usage:
/// ```dart
/// PhoneInputField(
/// controller: phoneController,
/// validator: Validators.phone,
/// onChanged: (value) {
/// // Handle phone number change
/// },
/// )
/// ```
class PhoneInputField extends StatelessWidget {
/// Creates a phone input field
const PhoneInputField({
super.key,
required this.controller,
this.focusNode,
this.validator,
this.onChanged,
this.onFieldSubmitted,
this.enabled = true,
this.autofocus = false,
});
/// Text editing controller
final TextEditingController controller;
/// Focus node for keyboard focus management
final FocusNode? focusNode;
/// Form field validator
final String? Function(String?)? validator;
/// Callback when text changes
final void Function(String)? onChanged;
/// Callback when field is submitted
final void Function(String)? onFieldSubmitted;
/// Whether the field is enabled
final bool enabled;
/// Whether the field should auto-focus
final bool autofocus;
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
focusNode: focusNode,
autofocus: autofocus,
enabled: enabled,
keyboardType: TextInputType.phone,
textInputAction: TextInputAction.next,
inputFormatters: [
// Allow digits, spaces, +, and parentheses
FilteringTextInputFormatter.allow(RegExp(r'[0-9+\s()]')),
// Limit to reasonable phone length
LengthLimitingTextInputFormatter(15),
],
style: const TextStyle(
fontSize: InputFieldSpecs.fontSize,
color: AppColors.grey900,
),
decoration: InputDecoration(
labelText: 'Số điện thoại',
labelStyle: const TextStyle(
fontSize: InputFieldSpecs.labelFontSize,
color: AppColors.grey500,
),
hintText: 'Nhập số điện thoại',
hintStyle: const TextStyle(
fontSize: InputFieldSpecs.hintFontSize,
color: AppColors.grey500,
),
prefixIcon: const Icon(
Icons.phone,
color: AppColors.primaryBlue,
size: AppIconSize.md,
),
filled: true,
fillColor: AppColors.white,
contentPadding: InputFieldSpecs.contentPadding,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
width: 2.0,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.danger, width: 1.0),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.danger, width: 2.0),
),
errorStyle: const TextStyle(fontSize: 12.0, color: AppColors.danger),
),
validator: validator,
onChanged: onChanged,
onFieldSubmitted: onFieldSubmitted,
);
}
}

View File

@@ -0,0 +1,115 @@
/// Role Dropdown Widget
///
/// Dropdown for selecting user role during registration.
library;
import 'package:flutter/material.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// Role Dropdown
///
/// A custom dropdown widget for selecting user role.
/// Roles:
/// - dealer: Đại lý hệ thống
/// - worker: Kiến trúc sư/ Thầu thợ
/// - broker: Khách lẻ
/// - other: Khác
///
/// Usage:
/// ```dart
/// RoleDropdown(
/// value: selectedRole,
/// onChanged: (value) {
/// setState(() {
/// selectedRole = value;
/// });
/// },
/// validator: (value) {
/// if (value == null || value.isEmpty) {
/// return 'Vui lòng chọn vai trò';
/// }
/// return null;
/// },
/// )
/// ```
class RoleDropdown extends StatelessWidget {
/// Creates a role dropdown
const RoleDropdown({
super.key,
required this.value,
required this.onChanged,
this.validator,
});
/// Selected role value
final String? value;
/// Callback when role changes
final void Function(String?) onChanged;
/// Form field validator
final String? Function(String?)? validator;
@override
Widget build(BuildContext context) {
return DropdownButtonFormField<String>(
value: value,
decoration: InputDecoration(
hintText: 'Chọn vai trò của bạn',
hintStyle: const TextStyle(
fontSize: InputFieldSpecs.hintFontSize,
color: AppColors.grey500,
),
prefixIcon: const Icon(
Icons.work,
color: AppColors.primaryBlue,
size: AppIconSize.md,
),
filled: true,
fillColor: AppColors.white,
contentPadding: InputFieldSpecs.contentPadding,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
width: 2.0,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.danger, width: 1.0),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.danger, width: 2.0),
),
),
items: const [
DropdownMenuItem(value: 'dealer', child: Text('Đại lý hệ thống')),
DropdownMenuItem(
value: 'worker',
child: Text('Kiến trúc sư/ Thầu thợ'),
),
DropdownMenuItem(value: 'broker', child: Text('Khách lẻ')),
DropdownMenuItem(value: 'other', child: Text('Khác')),
],
onChanged: onChanged,
validator: validator,
icon: const Icon(Icons.arrow_drop_down, color: AppColors.grey500),
dropdownColor: AppColors.white,
style: const TextStyle(
fontSize: InputFieldSpecs.fontSize,
color: AppColors.grey900,
),
);
}
}