From f9043173c0e705df23a677e0838806d78483e4cd Mon Sep 17 00:00:00 2001 From: Gaurav Kumar Date: Tue, 9 Sep 2025 08:45:46 +0530 Subject: [PATCH] modern --- .../shared/fields/audio_upload_field.dart | 86 +++ .../shared/fields/base_field.dart | 33 + .../shared/fields/captcha_field.dart | 162 +++++ .../shared/fields/checkbox_field.dart | 54 ++ .../shared/fields/custom_text_field.dart | 60 ++ .../shared/fields/date_field.dart | 76 +++ .../shared/fields/datetime_field.dart | 86 +++ .../shared/fields/dropdown_field.dart | 50 ++ .../shared/fields/email_field.dart | 48 ++ .../shared/fields/file_upload_field.dart | 85 +++ .../shared/fields/image_upload_field.dart | 103 +++ .../shared/fields/number_field.dart | 61 ++ .../shared/fields/password_field.dart | 63 ++ .../shared/fields/phone_field.dart | 51 ++ .../shared/fields/radio_field.dart | 65 ++ .../shared/fields/switch_field.dart | 53 ++ .../BuilderField/shared/fields/url_field.dart | 48 ++ .../shared/fields/video_upload_field.dart | 86 +++ .../BuilderField/shared/ui/entity_card.dart | 492 ++++++++++++++ .../shared/ui/entity_details.dart | 584 +++++++++++++++++ .../BuilderField/shared/ui/entity_form.dart | 146 +++++ .../BuilderField/shared/ui/entity_list.dart | 476 ++++++++++++++ .../shared/ui/entity_screens.dart | 608 ++++++++++++++++++ .../shared/utils/entity_field_store.dart | 37 ++ .../widgets/inputs/modern_text_field.dart | 23 +- 25 files changed, 3632 insertions(+), 4 deletions(-) create mode 100644 base_project/lib/BuilderField/shared/fields/audio_upload_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/base_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/captcha_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/checkbox_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/custom_text_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/date_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/datetime_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/dropdown_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/email_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/file_upload_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/image_upload_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/number_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/password_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/phone_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/radio_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/switch_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/url_field.dart create mode 100644 base_project/lib/BuilderField/shared/fields/video_upload_field.dart create mode 100644 base_project/lib/BuilderField/shared/ui/entity_card.dart create mode 100644 base_project/lib/BuilderField/shared/ui/entity_details.dart create mode 100644 base_project/lib/BuilderField/shared/ui/entity_form.dart create mode 100644 base_project/lib/BuilderField/shared/ui/entity_list.dart create mode 100644 base_project/lib/BuilderField/shared/ui/entity_screens.dart create mode 100644 base_project/lib/BuilderField/shared/utils/entity_field_store.dart diff --git a/base_project/lib/BuilderField/shared/fields/audio_upload_field.dart b/base_project/lib/BuilderField/shared/fields/audio_upload_field.dart new file mode 100644 index 0000000..22982bc --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/audio_upload_field.dart @@ -0,0 +1,86 @@ +import 'dart:typed_data'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import 'base_field.dart'; +import '../utils/entity_field_store.dart'; + +class AudioUploadField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + + AudioUploadField({ + required this.fieldKey, + required this.label, + this.hint = 'Select audio file', + this.isRequired = false, + }); + + @override + String? Function(String?)? get validator => (_) { + final items = + EntityFieldStore.instance.get>(fieldKey) ?? + const []; + if (isRequired && items.isEmpty) return '$label is required'; + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + final items = EntityFieldStore.instance.get>(fieldKey) ?? + []; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(label, + style: TextStyle( + fontWeight: FontWeight.w600, color: colorScheme.onSurface)), + const Spacer(), + TextButton.icon( + onPressed: () async { + final result = + await FilePicker.platform.pickFiles(type: FileType.audio); + if (result != null && result.files.isNotEmpty) { + final f = result.files.single; + final Uint8List? bytes = f.bytes ?? + (f.path != null + ? await File(f.path!).readAsBytes() + : null); + if (bytes != null) { + final updated = List.from(items) + ..add(UploadItem(fileName: f.name, bytes: bytes)); + EntityFieldStore.instance.set(fieldKey, updated); + if (onChanged != null) onChanged(); + } + } + }, + icon: const Icon(Icons.audiotrack_rounded), + label: const Text('Add Audio'), + ), + ], + ), + const SizedBox(height: 8), + ...items.map((u) => ListTile( + leading: const Icon(Icons.music_note_rounded), + title: Text(u.fileName, overflow: TextOverflow.ellipsis), + trailing: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () { + final updated = List.from(items)..remove(u); + EntityFieldStore.instance.set(fieldKey, updated); + if (onChanged != null) onChanged(); + }, + ), + )), + ], + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/base_field.dart b/base_project/lib/BuilderField/shared/fields/base_field.dart new file mode 100644 index 0000000..66e6499 --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/base_field.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +/// Base interface for all field types in the Entity system +/// This allows UI to be independent of field types and enables reusability +abstract class BaseField { + /// Unique identifier for the field (e.g., 'name', 'phone') + String get fieldKey; + + /// Display label for the field (e.g., 'Name', 'Phone Number') + String get label; + + /// Placeholder text for the field + String get hint; + + /// Whether the field is required for validation + bool get isRequired; + + /// Validation function for the field + String? Function(String?)? get validator; + + /// Main method - each field type provides its own implementation + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }); + + /// Optional: Field-specific styling properties + Map? get customProperties => null; + + /// Optional: Field-specific validation rules + String? Function(String?)? get customValidator => null; +} diff --git a/base_project/lib/BuilderField/shared/fields/captcha_field.dart b/base_project/lib/BuilderField/shared/fields/captcha_field.dart new file mode 100644 index 0000000..b1114b2 --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/captcha_field.dart @@ -0,0 +1,162 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; + +import '../../../shared/widgets/inputs/modern_text_field.dart'; +import 'base_field.dart'; + +/// Generic Captcha field: shows generated code and asks user to type it +class CaptchaField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + final int length; + + CaptchaField({ + required this.fieldKey, + required this.label, + this.hint = 'Enter CAPTCHA', + this.isRequired = true, + this.length = 6, + }); + + @override + String? Function(String?)? get validator => (value) { + if (isRequired && (value == null || value.isEmpty)) { + return '$label is required'; + } + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + return _CaptchaFieldWidget( + key: ValueKey(fieldKey), + label: label, + hint: hint, + length: length, + colorScheme: colorScheme, + controller: controller, + baseValidator: validator, + onChanged: onChanged, + ); + } + + String _generateCaptcha(int length) { + final random = Random(); + const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; + return String.fromCharCodes(Iterable.generate( + length, + (_) => chars.codeUnitAt(random.nextInt(chars.length)), + )); + } +} + +class _CaptchaFieldWidget extends StatefulWidget { + final String label; + final String hint; + final int length; + final ColorScheme colorScheme; + final TextEditingController controller; + final String? Function(String?)? baseValidator; + final VoidCallback? onChanged; + + const _CaptchaFieldWidget({ + super.key, + required this.label, + required this.hint, + required this.length, + required this.colorScheme, + required this.controller, + required this.baseValidator, + required this.onChanged, + }); + + @override + State<_CaptchaFieldWidget> createState() => _CaptchaFieldWidgetState(); +} + +class _CaptchaFieldWidgetState extends State<_CaptchaFieldWidget> { + late String _captcha; + + @override + void initState() { + super.initState(); + _captcha = _generateCaptcha(widget.length); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: widget.colorScheme.primaryContainer.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: widget.colorScheme.primary.withOpacity(0.2)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + _captcha, + style: TextStyle( + color: widget.colorScheme.primary, + fontSize: 20, + fontWeight: FontWeight.w700, + letterSpacing: 3, + ), + ), + IconButton( + tooltip: 'Refresh', + onPressed: () { + setState(() { + _captcha = _generateCaptcha(widget.length); + }); + if (widget.onChanged != null) widget.onChanged!(); + }, + icon: Icon(Icons.refresh_rounded, + color: widget.colorScheme.primary), + ), + ], + ), + ), + const SizedBox(height: 12), + ModernTextField( + controller: widget.controller, + hint: widget.hint, + validator: (value) { + final base = widget.baseValidator; + final err = base != null ? base(value) : null; + if (err != null) return err; + if ((value ?? '').trim() != _captcha) { + return 'CAPTCHA does not match'; + } + return null; + }, + onChanged: + widget.onChanged != null ? (_) => widget.onChanged!() : null, + ), + ], + ); + } + + String _generateCaptcha(int length) { + final random = Random(); + const chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz'; + return String.fromCharCodes(Iterable.generate( + length, + (_) => chars.codeUnitAt(random.nextInt(chars.length)), + )); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/checkbox_field.dart b/base_project/lib/BuilderField/shared/fields/checkbox_field.dart new file mode 100644 index 0000000..410698e --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/checkbox_field.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'base_field.dart'; + +class CheckboxField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + + CheckboxField({ + required this.fieldKey, + required this.label, + this.hint = '', + this.isRequired = false, + }); + + @override + String? Function(String?)? get validator => (value) { + if (isRequired && (value == null || value.isEmpty)) { + return '$label is required'; + } + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + final bool checked = controller.text.toLowerCase() == 'true'; + return Row( + children: [ + Checkbox( + value: checked, + onChanged: (value) { + controller.text = (value ?? false).toString(); + if (onChanged != null) onChanged(); + }, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + label, + style: TextStyle( + color: colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/custom_text_field.dart b/base_project/lib/BuilderField/shared/fields/custom_text_field.dart new file mode 100644 index 0000000..b92ee38 --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/custom_text_field.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'base_field.dart'; +import '../../../shared/widgets/inputs/modern_text_field.dart'; + +/// Custom text input field implementation (preferred over legacy TextField) +class CustomTextField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + final int? maxLength; + final TextInputType? keyboardType; + final Map? customProperties; + final String? Function(String?)? customValidator; + + CustomTextField({ + required this.fieldKey, + required this.label, + required this.hint, + this.isRequired = false, + this.maxLength, + this.keyboardType, + this.customProperties, + this.customValidator, + }); + + @override + String? Function(String?)? get validator => (value) { + // Custom validator (e.g., range checks) + if (customValidator != null) { + final err = customValidator!(value); + if (err != null) return err; + } + if (isRequired && (value == null || value.isEmpty)) { + return '$label is required'; + } + if (maxLength != null && value != null && value.length > maxLength!) { + return '$label must be less than $maxLength characters'; + } + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + final bool isPassword = customProperties?['isPassword'] == true; + return ModernTextField( + controller: controller, + hint: hint, + maxLength: maxLength, + keyboardType: keyboardType, + obscureText: isPassword, + onChanged: onChanged != null ? (_) => onChanged() : null, + validator: validator, + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/date_field.dart b/base_project/lib/BuilderField/shared/fields/date_field.dart new file mode 100644 index 0000000..9c31ebc --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/date_field.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; + +import '../../../shared/widgets/inputs/modern_text_field.dart'; +import 'base_field.dart'; + +/// Date input field implementation with native date picker +class DateField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + final DateTime? initialDate; + final DateTime firstDate; + final DateTime lastDate; + + DateField({ + required this.fieldKey, + required this.label, + required this.hint, + this.isRequired = false, + this.initialDate, + DateTime? firstDate, + DateTime? lastDate, + }) : firstDate = firstDate ?? DateTime(2000), + lastDate = lastDate ?? DateTime(2101); + + @override + String? Function(String?)? get validator => (value) { + if (isRequired && (value == null || value.isEmpty)) { + return '$label is required'; + } + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + return Builder( + builder: (context) { + return ModernTextField( + controller: controller, + hint: hint.isNotEmpty ? hint : label, + readOnly: true, + validator: validator, + suffixIcon: const Icon(Icons.calendar_today_rounded), + onTap: () async { + FocusScope.of(context).unfocus(); + final now = DateTime.now(); + final picked = await showDatePicker( + context: context, + initialDate: initialDate ?? now, + firstDate: firstDate, + lastDate: lastDate, + helpText: label, + ); + if (picked != null) { + final String value = _formatDate(picked); + controller.text = value; + if (onChanged != null) onChanged(); + } + }, + ); + }, + ); + } + + String _formatDate(DateTime date) { + String two(int n) => n < 10 ? '0$n' : '$n'; + return '${date.year}-${two(date.month)}-${two(date.day)}'; + } +} + +// No global navigator key required; Builder provides local context diff --git a/base_project/lib/BuilderField/shared/fields/datetime_field.dart b/base_project/lib/BuilderField/shared/fields/datetime_field.dart new file mode 100644 index 0000000..98f9e9f --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/datetime_field.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'base_field.dart'; +import 'package:flutter/material.dart'; +import 'base_field.dart'; +import '../../../shared/widgets/inputs/modern_text_field.dart'; + +/// DateTime input field implementation with native date & time pickers +class DateTimeField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + final DateTime? initialDateTime; + final DateTime firstDate; + final DateTime lastDate; + + DateTimeField({ + required this.fieldKey, + required this.label, + required this.hint, + this.isRequired = false, + this.initialDateTime, + DateTime? firstDate, + DateTime? lastDate, + }) : firstDate = firstDate ?? DateTime(2000), + lastDate = lastDate ?? DateTime(2101); + + @override + String? Function(String?)? get validator => (value) { + if (isRequired && (value == null || value.isEmpty)) { + return '$label is required'; + } + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + return Builder( + builder: (context) { + return ModernTextField( + controller: controller, + hint: hint.isNotEmpty ? hint : label, + readOnly: true, + validator: validator, + suffixIcon: const Icon(Icons.access_time_rounded), + onTap: () async { + FocusScope.of(context).unfocus(); + final now = DateTime.now(); + final datePicked = await showDatePicker( + context: context, + initialDate: (initialDateTime ?? now), + firstDate: firstDate, + lastDate: lastDate, + helpText: label, + ); + if (datePicked == null) return; + final timePicked = await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(initialDateTime ?? now), + helpText: label, + ); + if (timePicked == null) return; + final combined = DateTime( + datePicked.year, + datePicked.month, + datePicked.day, + timePicked.hour, + timePicked.minute, + ); + controller.text = _formatDateTime(combined); + if (onChanged != null) onChanged(); + }, + ); + }, + ); + } + + String _formatDateTime(DateTime dt) { + String two(int n) => n < 10 ? '0$n' : '$n'; + return '${dt.year}-${two(dt.month)}-${two(dt.day)} ${two(dt.hour)}:${two(dt.minute)}'; + } +} diff --git a/base_project/lib/BuilderField/shared/fields/dropdown_field.dart b/base_project/lib/BuilderField/shared/fields/dropdown_field.dart new file mode 100644 index 0000000..10a9498 --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/dropdown_field.dart @@ -0,0 +1,50 @@ +// import 'package:flutter/material.dart'; +// import 'base_field.dart'; +// import '../../../Reuseable/reusable_dropdown_field.dart'; + +// /// Dropdown selection field implementation +// class DropdownField extends BaseField { +// final String fieldKey; +// final String label; +// final String hint; +// final bool isRequired; +// final List> options; +// final String valueKey; +// final String displayKey; + +// DropdownField({ +// required this.fieldKey, +// required this.label, +// required this.hint, +// this.isRequired = false, +// required this.options, +// this.valueKey = 'id', +// this.displayKey = 'name', +// }); + +// @override +// String? Function(String?)? get validator => (value) { +// if (isRequired && (value == null || value.isEmpty)) { +// return '$label is required'; +// } +// return null; +// }; + +// @override +// Widget buildField({ +// required TextEditingController controller, +// required ColorScheme colorScheme, +// VoidCallback? onChanged, +// }) { +// return ReusableDropdownField( +// controller: controller, +// label: label, +// hint: hint, +// items: options, +// valueKey: valueKey, +// displayKey: displayKey, +// onChanged: onChanged != null ? (_) => onChanged() : null, +// validator: validator, +// ); +// } +// } diff --git a/base_project/lib/BuilderField/shared/fields/email_field.dart b/base_project/lib/BuilderField/shared/fields/email_field.dart new file mode 100644 index 0000000..8876b3c --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/email_field.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'base_field.dart'; +import '../../../shared/widgets/inputs/modern_text_field.dart'; + +/// Email input field implementation +class EmailField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + + EmailField({ + required this.fieldKey, + required this.label, + required this.hint, + this.isRequired = false, + }); + + @override + String? Function(String?)? get validator => (value) { + if (isRequired && (value == null || value.isEmpty)) { + return '$label is required'; + } + if (value != null && value.isNotEmpty) { + final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); + if (!emailRegex.hasMatch(value)) { + return 'Please enter a valid email address'; + } + } + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + return ModernTextField( + controller: controller, + hint: hint, + keyboardType: TextInputType.emailAddress, + prefixIcon: const Icon(Icons.email_outlined), + onChanged: onChanged != null ? (_) => onChanged() : null, + validator: validator, + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/file_upload_field.dart b/base_project/lib/BuilderField/shared/fields/file_upload_field.dart new file mode 100644 index 0000000..a41e91c --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/file_upload_field.dart @@ -0,0 +1,85 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import 'base_field.dart'; +import '../utils/entity_field_store.dart'; + +/// Multi-file upload field; stores files in EntityFieldStore under fieldKey +class FileUploadField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + + FileUploadField({ + required this.fieldKey, + required this.label, + this.hint = 'Select files', + this.isRequired = false, + }); + + @override + String? Function(String?)? get validator => (_) { + final items = + EntityFieldStore.instance.get>(fieldKey) ?? + const []; + if (isRequired && items.isEmpty) { + return '$label is required'; + } + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + final items = EntityFieldStore.instance.get>(fieldKey) ?? + []; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(label, + style: TextStyle( + fontWeight: FontWeight.w600, color: colorScheme.onSurface)), + const Spacer(), + TextButton.icon( + onPressed: () async { + final result = + await FilePicker.platform.pickFiles(allowMultiple: true); + if (result != null) { + final List updated = List.from(items); + for (final f in result.files) { + final Uint8List? bytes = f.bytes; + if (bytes == null) continue; + updated.add(UploadItem(fileName: f.name, bytes: bytes)); + } + EntityFieldStore.instance.set(fieldKey, updated); + if (onChanged != null) onChanged(); + } + }, + icon: const Icon(Icons.attach_file_rounded), + label: const Text('Add Files'), + ), + ], + ), + const SizedBox(height: 8), + ...items.map((u) => ListTile( + leading: const Icon(Icons.insert_drive_file_rounded), + title: Text(u.fileName, overflow: TextOverflow.ellipsis), + trailing: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () { + final updated = List.from(items)..remove(u); + EntityFieldStore.instance.set(fieldKey, updated); + if (onChanged != null) onChanged(); + }, + ), + )), + ], + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/image_upload_field.dart b/base_project/lib/BuilderField/shared/fields/image_upload_field.dart new file mode 100644 index 0000000..750a2c0 --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/image_upload_field.dart @@ -0,0 +1,103 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; +import 'base_field.dart'; +import '../utils/entity_field_store.dart'; + +class ImageUploadField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + + ImageUploadField({ + required this.fieldKey, + required this.label, + this.hint = 'Select images', + this.isRequired = false, + }); + + @override + String? Function(String?)? get validator => (_) { + final items = + EntityFieldStore.instance.get>(fieldKey) ?? + const []; + if (isRequired && items.isEmpty) return '$label is required'; + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + final items = EntityFieldStore.instance.get>(fieldKey) ?? + []; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(label, + style: TextStyle( + fontWeight: FontWeight.w600, color: colorScheme.onSurface)), + const Spacer(), + TextButton.icon( + onPressed: () async { + final ImagePicker picker = ImagePicker(); + final XFile? picked = + await picker.pickImage(source: ImageSource.gallery); + if (picked != null) { + final Uint8List bytes = await picked.readAsBytes(); + final updated = List.from(items) + ..add(UploadItem(fileName: picked.name, bytes: bytes)); + EntityFieldStore.instance.set(fieldKey, updated); + if (onChanged != null) onChanged(); + } + }, + icon: const Icon(Icons.image_rounded), + label: const Text('Add Image'), + ), + ], + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: items + .map((u) => Stack( + alignment: Alignment.topRight, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Image.memory(u.bytes, + width: 72, height: 72, fit: BoxFit.cover), + ), + Positioned( + right: 0, + child: InkWell( + onTap: () { + final updated = List.from(items) + ..remove(u); + EntityFieldStore.instance.set(fieldKey, updated); + if (onChanged != null) onChanged(); + }, + child: Container( + decoration: BoxDecoration( + color: Colors.black54, + borderRadius: BorderRadius.circular(8), + ), + child: const Icon(Icons.close_rounded, + color: Colors.white, size: 18), + ), + ), + ), + ], + )) + .toList(), + ), + ], + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/number_field.dart b/base_project/lib/BuilderField/shared/fields/number_field.dart new file mode 100644 index 0000000..ced8883 --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/number_field.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'base_field.dart'; +import '../../../shared/widgets/inputs/modern_text_field.dart'; + +/// Number input field implementation +class NumberField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + final double? min; + final double? max; + final int? decimalPlaces; + + NumberField({ + required this.fieldKey, + required this.label, + required this.hint, + this.isRequired = false, + this.min, + this.max, + this.decimalPlaces, + }); + + @override + String? Function(String?)? get validator => (value) { + if (isRequired && (value == null || value.isEmpty)) { + return '$label is required'; + } + if (value != null && value.isNotEmpty) { + final number = double.tryParse(value); + if (number == null) { + return 'Please enter a valid number'; + } + if (min != null && number < min!) { + return '$label must be at least $min'; + } + if (max != null && number > max!) { + return '$label must be at most $max'; + } + } + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + return ModernTextField( + controller: controller, + hint: hint, + keyboardType: TextInputType.numberWithOptions( + decimal: decimalPlaces != null && decimalPlaces! > 0, + ), + onChanged: onChanged != null ? (_) => onChanged() : null, + validator: validator, + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/password_field.dart b/base_project/lib/BuilderField/shared/fields/password_field.dart new file mode 100644 index 0000000..9bccfff --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/password_field.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'base_field.dart'; +import '../../../shared/widgets/inputs/modern_text_field.dart'; + +/// Reusable password field supporting dynamic pairing with confirm password +/// via a shared groupId. Use two instances with the same groupId, one with +/// isConfirm=true. EntityForm will handle cross-field validation and exclude +/// confirm entries from submission. +class PasswordField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + final int? maxLength; + final String? groupId; // Pair password/confirm dynamically + final bool isConfirm; // Mark this instance as a confirm field + + PasswordField({ + required this.fieldKey, + required this.label, + required this.hint, + this.isRequired = false, + this.maxLength, + this.groupId, + this.isConfirm = false, + }); + + @override + String? Function(String?)? get validator => (value) { + if (isRequired && (value == null || value.isEmpty)) { + return '$label is required'; + } + if (maxLength != null && value != null && value.length > maxLength!) { + return '$label must be less than $maxLength characters'; + } + // Cross-field match is handled centrally in EntityForm using groupId + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + return ModernTextField( + controller: controller, + hint: hint, + maxLength: maxLength, + keyboardType: TextInputType.visiblePassword, + obscureText: true, + onChanged: onChanged != null ? (_) => onChanged() : null, + validator: validator, + ); + } + + @override + Map? get customProperties => { + if (groupId != null) 'groupId': groupId, + 'isConfirm': isConfirm, + 'isPassword': true, + }; +} diff --git a/base_project/lib/BuilderField/shared/fields/phone_field.dart b/base_project/lib/BuilderField/shared/fields/phone_field.dart new file mode 100644 index 0000000..93ff36d --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/phone_field.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'base_field.dart'; +import '../../../shared/widgets/inputs/modern_text_field.dart'; + +/// Phone number input field implementation +class PhoneField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + final String? countryCode; + + PhoneField({ + required this.fieldKey, + required this.label, + required this.hint, + this.isRequired = false, + this.countryCode, + }); + + @override + String? Function(String?)? get validator => (value) { + if (isRequired && (value == null || value.isEmpty)) { + return '$label is required'; + } + if (value != null && value.isNotEmpty) { + // Phone number validation + final phoneRegex = RegExp(r'^\+?[\d\s\-\(\)]+$'); + if (!phoneRegex.hasMatch(value)) { + return 'Please enter a valid phone number'; + } + } + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + return ModernTextField( + controller: controller, + hint: hint, + keyboardType: TextInputType.phone, + prefixIcon: countryCode != null ? Text(countryCode!) : null, + onChanged: onChanged != null ? (_) => onChanged() : null, + validator: validator, + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/radio_field.dart b/base_project/lib/BuilderField/shared/fields/radio_field.dart new file mode 100644 index 0000000..2baf629 --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/radio_field.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'base_field.dart'; + +class RadioField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + final List options; // e.g., ['yes','no'] + + RadioField({ + required this.fieldKey, + required this.label, + this.hint = '', + this.isRequired = false, + required this.options, + }); + + @override + String? Function(String?)? get validator => (value) { + if (isRequired && (value == null || value.isEmpty)) { + return '$label is required'; + } + if (value != null && value.isNotEmpty && !options.contains(value)) { + return 'Invalid selection'; + } + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + final String current = controller.text; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + color: colorScheme.onSurface, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 6), + Wrap( + spacing: 12, + children: options.map((opt) { + return ChoiceChip( + label: Text(opt), + selected: current == opt, + selectedColor: colorScheme.primary.withOpacity(0.2), + onSelected: (_) { + controller.text = opt; + if (onChanged != null) onChanged(); + }, + ); + }).toList(), + ), + ], + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/switch_field.dart b/base_project/lib/BuilderField/shared/fields/switch_field.dart new file mode 100644 index 0000000..86f40bf --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/switch_field.dart @@ -0,0 +1,53 @@ +import 'package:flutter/material.dart'; +import 'base_field.dart'; + +/// Boolean switch field implementation +class SwitchField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + + SwitchField({ + required this.fieldKey, + required this.label, + this.hint = '', + this.isRequired = false, + }); + + @override + String? Function(String?)? get validator => (_) => null; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + final bool current = (controller.text.toLowerCase() == 'true'); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + label, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ), + Switch( + value: current, + activeColor: colorScheme.onPrimary, + activeTrackColor: colorScheme.primary, + onChanged: (value) { + controller.text = value.toString(); + if (onChanged != null) onChanged(); + }, + ), + ], + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/url_field.dart b/base_project/lib/BuilderField/shared/fields/url_field.dart new file mode 100644 index 0000000..f3acd5f --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/url_field.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'base_field.dart'; +import '../../../shared/widgets/inputs/modern_text_field.dart'; + +/// URL input field implementation using ModernTextField +class UrlField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + + UrlField({ + required this.fieldKey, + required this.label, + required this.hint, + this.isRequired = false, + }); + + @override + String? Function(String?)? get validator => (value) { + if (isRequired && (value == null || value.isEmpty)) { + return '$label is required'; + } + if (value != null && value.isNotEmpty) { + final uri = Uri.tryParse(value); + if (uri == null || !(uri.hasScheme && uri.hasAuthority)) { + return 'Please enter a valid URL'; + } + } + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + return ModernTextField( + controller: controller, + hint: hint, + keyboardType: TextInputType.url, + prefixIcon: const Icon(Icons.link_rounded), + validator: validator, + onChanged: onChanged != null ? (_) => onChanged() : null, + ); + } +} diff --git a/base_project/lib/BuilderField/shared/fields/video_upload_field.dart b/base_project/lib/BuilderField/shared/fields/video_upload_field.dart new file mode 100644 index 0000000..329abda --- /dev/null +++ b/base_project/lib/BuilderField/shared/fields/video_upload_field.dart @@ -0,0 +1,86 @@ +import 'dart:typed_data'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:file_picker/file_picker.dart'; +import 'base_field.dart'; +import '../utils/entity_field_store.dart'; + +class VideoUploadField extends BaseField { + final String fieldKey; + final String label; + final String hint; + final bool isRequired; + + VideoUploadField({ + required this.fieldKey, + required this.label, + this.hint = 'Select video file', + this.isRequired = false, + }); + + @override + String? Function(String?)? get validator => (_) { + final items = + EntityFieldStore.instance.get>(fieldKey) ?? + const []; + if (isRequired && items.isEmpty) return '$label is required'; + return null; + }; + + @override + Widget buildField({ + required TextEditingController controller, + required ColorScheme colorScheme, + VoidCallback? onChanged, + }) { + final items = EntityFieldStore.instance.get>(fieldKey) ?? + []; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text(label, + style: TextStyle( + fontWeight: FontWeight.w600, color: colorScheme.onSurface)), + const Spacer(), + TextButton.icon( + onPressed: () async { + final result = + await FilePicker.platform.pickFiles(type: FileType.video); + if (result != null && result.files.isNotEmpty) { + final f = result.files.single; + final Uint8List? bytes = f.bytes ?? + (f.path != null + ? await File(f.path!).readAsBytes() + : null); + if (bytes != null) { + final updated = List.from(items) + ..add(UploadItem(fileName: f.name, bytes: bytes)); + EntityFieldStore.instance.set(fieldKey, updated); + if (onChanged != null) onChanged(); + } + } + }, + icon: const Icon(Icons.video_library_rounded), + label: const Text('Add Video'), + ), + ], + ), + const SizedBox(height: 8), + ...items.map((u) => ListTile( + leading: const Icon(Icons.videocam_rounded), + title: Text(u.fileName, overflow: TextOverflow.ellipsis), + trailing: IconButton( + icon: const Icon(Icons.close_rounded), + onPressed: () { + final updated = List.from(items)..remove(u); + EntityFieldStore.instance.set(fieldKey, updated); + if (onChanged != null) onChanged(); + }, + ), + )), + ], + ); + } +} diff --git a/base_project/lib/BuilderField/shared/ui/entity_card.dart b/base_project/lib/BuilderField/shared/ui/entity_card.dart new file mode 100644 index 0000000..4bb19a0 --- /dev/null +++ b/base_project/lib/BuilderField/shared/ui/entity_card.dart @@ -0,0 +1,492 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../core/constants/ui_constants.dart'; +import '../../../core/providers/dynamic_theme_provider.dart'; + +/// Reusable card component for displaying entity data +/// Uses dynamic theme and provides consistent styling across all entities +class EntityCard extends StatelessWidget { + final Map entity; + final Function(Map) onEdit; + final Function(Map) onDelete; + final Function(Map)? onTap; + final List> displayFields; + + const EntityCard({ + super.key, + required this.entity, + required this.onEdit, + required this.onDelete, + this.onTap, + this.displayFields = const [], + }); + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, dynamicThemeProvider, child) { + final colorScheme = dynamicThemeProvider.getCurrentColorScheme( + Theme.of(context).brightness == Brightness.dark, + ); + final size = MediaQuery.of(context).size; + final isMobile = size.width < 600; + final isTablet = size.width >= 600 && size.width < 1024; + final double maxCardHeight = isMobile + ? size.height * 0.34 + : isTablet + ? size.height * 0.38 + : size.height * 0.42; + final double horizontalPadding = isMobile + ? UIConstants.spacing16 + : isTablet + ? UIConstants.spacing20 + : UIConstants.spacing24; + final double verticalPadding = isMobile + ? UIConstants.spacing16 + : isTablet + ? UIConstants.spacing16 + : UIConstants.spacing20; + + return ConstrainedBox( + constraints: BoxConstraints( + minHeight: 160, + maxHeight: maxCardHeight.clamp(160.0, size.height * 0.8), + ), + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.surface, + colorScheme.surface.withOpacity(0.95), + ], + ), + borderRadius: BorderRadius.circular(UIConstants.radius20), + border: Border.all( + color: colorScheme.primary.withOpacity(0.1), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 2, + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap != null ? () => onTap!(entity) : null, + borderRadius: BorderRadius.circular(UIConstants.radius20), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: horizontalPadding, + vertical: verticalPadding, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header with avatar and action buttons + Row( + children: [ + _buildAvatar(colorScheme, + isMobile: isMobile, isTablet: isTablet), + const Spacer(), + _buildActionButtons(colorScheme), + ], + ), + const SizedBox(height: UIConstants.spacing16), + + // Dynamic field display (scrollable to avoid overflow) + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.zero, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _buildFieldDisplays(colorScheme), + ), + ), + ), + const SizedBox(height: UIConstants.spacing12), + // Status indicator pinned at bottom + _buildStatusIndicator(colorScheme), + ], + ), + ), + ), + ), + ), + ); + }, + ); + } + + Widget _buildAvatar(ColorScheme colorScheme, + {required bool isMobile, required bool isTablet}) { + final double side = isMobile + ? 44 + : isTablet + ? 50 + : 56; + final double icon = isMobile + ? 20 + : isTablet + ? 22 + : 24; + return Container( + width: side, + height: side, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primary, + colorScheme.primary.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(UIConstants.radius12), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(0.3), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Icon( + Icons.person_rounded, + color: colorScheme.onPrimary, + size: icon, + ), + ); + } + + Widget _buildActionButtons(ColorScheme colorScheme) { + return Row( + children: [ + // Edit Button + Container( + decoration: BoxDecoration( + color: colorScheme.primaryContainer.withOpacity(0.3), + borderRadius: BorderRadius.circular(UIConstants.radius8), + border: Border.all( + color: colorScheme.primary.withOpacity(0.2), + width: 1, + ), + ), + child: IconButton( + onPressed: () => onEdit(entity), + icon: Icon( + Icons.edit_rounded, + color: colorScheme.primary, + size: 18, + ), + tooltip: 'Edit', + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + ), + ), + const SizedBox(width: UIConstants.spacing8), + // Delete Button + Container( + decoration: BoxDecoration( + color: colorScheme.errorContainer.withOpacity(0.3), + borderRadius: BorderRadius.circular(UIConstants.radius8), + border: Border.all( + color: colorScheme.error.withOpacity(0.2), + width: 1, + ), + ), + child: IconButton( + onPressed: () => onDelete(entity), + icon: Icon( + Icons.delete_rounded, + color: colorScheme.error, + size: 18, + ), + tooltip: 'Delete', + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints( + minWidth: 32, + minHeight: 32, + ), + ), + ), + ], + ); + } + + List _buildFieldDisplays(ColorScheme colorScheme) { + // Dynamic field display based on entity data + final displayFields = _getDisplayFields(); + + return displayFields.map((field) { + final value = entity[field['key']]?.toString() ?? ''; + if (value.isEmpty) return const SizedBox.shrink(); + + return Padding( + padding: const EdgeInsets.only(bottom: UIConstants.spacing8), + child: _buildFieldDisplay(field, value, colorScheme), + ); + }).toList(); + } + + Widget _buildFieldDisplay( + Map field, String value, ColorScheme colorScheme) { + switch (field['type']) { + case 'phone': + return _buildPhoneDisplay(value, colorScheme); + case 'email': + return _buildEmailDisplay(value, colorScheme); + case 'number': + return _buildNumberDisplay(field['label'], value, colorScheme); + default: + return _buildTextDisplay(field['label'], value, colorScheme); + } + } + + Widget _buildPhoneDisplay(String value, ColorScheme colorScheme) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.spacing12, + vertical: UIConstants.spacing8, + ), + decoration: BoxDecoration( + color: colorScheme.primaryContainer.withOpacity(0.2), + borderRadius: BorderRadius.circular(UIConstants.radius8), + border: Border.all( + color: colorScheme.primary.withOpacity(0.1), + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.phone_rounded, + size: 16, + color: colorScheme.primary, + ), + const SizedBox(width: UIConstants.spacing8), + Expanded( + child: Text( + value, + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.8), + fontSize: 13, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + Widget _buildEmailDisplay(String value, ColorScheme colorScheme) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.spacing12, + vertical: UIConstants.spacing8, + ), + decoration: BoxDecoration( + color: colorScheme.secondaryContainer.withOpacity(0.2), + borderRadius: BorderRadius.circular(UIConstants.radius8), + border: Border.all( + color: colorScheme.secondary.withOpacity(0.1), + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.email_rounded, + size: 16, + color: colorScheme.secondary, + ), + const SizedBox(width: UIConstants.spacing8), + Expanded( + child: Text( + value, + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.8), + fontSize: 13, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + Widget _buildTextDisplay( + String label, String value, ColorScheme colorScheme) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: 16, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + if (label.isNotEmpty) ...[ + const SizedBox(height: UIConstants.spacing4), + Text( + label, + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.7), + fontSize: 12, + fontWeight: FontWeight.w400, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ); + } + + Widget _buildNumberDisplay( + String label, String value, ColorScheme colorScheme) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.spacing12, + vertical: UIConstants.spacing8, + ), + decoration: BoxDecoration( + color: colorScheme.tertiaryContainer.withOpacity(0.2), + borderRadius: BorderRadius.circular(UIConstants.radius8), + border: Border.all( + color: colorScheme.tertiary.withOpacity(0.1), + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.numbers_rounded, + size: 16, + color: colorScheme.tertiary, + ), + const SizedBox(width: UIConstants.spacing8), + Expanded( + child: Text( + value, + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.8), + fontSize: 13, + fontWeight: FontWeight.w500, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ); + } + + Widget _buildStatusIndicator(ColorScheme colorScheme) { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.spacing8, + vertical: UIConstants.spacing4, + ), + decoration: BoxDecoration( + color: colorScheme.primary.withOpacity(0.1), + borderRadius: BorderRadius.circular(UIConstants.radius12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: colorScheme.primary, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: UIConstants.spacing6), + Text( + 'Active', + style: TextStyle( + color: colorScheme.primary, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } + + List> _getDisplayFields() { + // Use provided displayFields or default fields + if (displayFields.isNotEmpty) { + return displayFields; + } + + // Default fields - try to get meaningful fields from entity + final defaultFields = >[]; + + // Try to find primary fields + final primaryFields = ['name', 'title', 'id']; + for (final field in primaryFields) { + if (entity.containsKey(field) && entity[field] != null) { + defaultFields.add({'key': field, 'label': '', 'type': 'text'}); + break; // Only add first found primary field + } + } + + // Add other non-empty fields (max 3 for card display) + int addedCount = 0; + for (final entry in entity.entries) { + if (entry.value != null && + entry.value.toString().isNotEmpty && + !primaryFields.contains(entry.key) && + addedCount < 2) { + defaultFields.add({ + 'key': entry.key, + 'label': _formatFieldLabel(entry.key), + 'type': _getFieldType(entry.value) + }); + addedCount++; + } + } + + return defaultFields; + } + + String _formatFieldLabel(String key) { + return key + .split('_') + .map((word) => word[0].toUpperCase() + word.substring(1)) + .join(' '); + } + + String _getFieldType(dynamic value) { + if (value is num) return 'number'; + if (value.toString().contains('@')) return 'email'; + if (RegExp(r'^\+?[\d\s\-\(\)]+$').hasMatch(value.toString())) + return 'phone'; + return 'text'; + } +} diff --git a/base_project/lib/BuilderField/shared/ui/entity_details.dart b/base_project/lib/BuilderField/shared/ui/entity_details.dart new file mode 100644 index 0000000..d175bc1 --- /dev/null +++ b/base_project/lib/BuilderField/shared/ui/entity_details.dart @@ -0,0 +1,584 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../core/providers/dynamic_theme_provider.dart'; +import '../../../core/constants/ui_constants.dart'; +import '../../../shared/widgets/app_bar/modern_app_bar.dart'; +import '../../../shared/widgets/buttons/modern_button.dart'; + +/// Generic details component for displaying entity information +/// This component works with any entity type and provides consistent UI/UX +class EntityDetails extends StatefulWidget { + final Map entity; + final Function(Map) onEdit; + final Function(Map) onDelete; + final String title; + final List> displayFields; + final bool isLoading; + + const EntityDetails({ + super.key, + required this.entity, + required this.onEdit, + required this.onDelete, + required this.title, + this.displayFields = const [], + this.isLoading = false, + }); + + @override + State createState() => _EntityDetailsState(); +} + +class _EntityDetailsState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + _initializeAnimations(); + } + + void _initializeAnimations() { + _animationController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0.0, 0.6, curve: Curves.easeOut), + )); + + _slideAnimation = Tween( + begin: const Offset(0, 0.3), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0.2, 1.0, curve: Curves.easeOutCubic), + )); + + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, dynamicThemeProvider, child) { + final colorScheme = dynamicThemeProvider.getCurrentColorScheme( + Theme.of(context).brightness == Brightness.dark, + ); + + return Scaffold( + backgroundColor: colorScheme.surface, + appBar: ModernAppBar( + title: widget.title, + showBackButton: true, + actions: [ + IconButton( + onPressed: () => widget.onEdit(widget.entity), + icon: Icon( + Icons.edit_rounded, + color: colorScheme.primary, + ), + tooltip: 'Edit', + ), + IconButton( + onPressed: () => _showDeleteDialog(colorScheme), + icon: Icon( + Icons.delete_rounded, + color: colorScheme.error, + ), + tooltip: 'Delete', + ), + ], + ), + body: widget.isLoading + ? _buildLoadingState(colorScheme) + : _buildContent(colorScheme), + ); + }, + ); + } + + Widget _buildLoadingState(ColorScheme colorScheme) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: colorScheme.primary, + strokeWidth: 3, + ), + const SizedBox(height: UIConstants.spacing16), + Text( + 'Loading details...', + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.7), + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildContent(ColorScheme colorScheme) { + return SingleChildScrollView( + padding: UIConstants.getResponsivePadding( + context, + mobile: UIConstants.screenPaddingMedium, + tablet: UIConstants.screenPaddingLarge, + desktop: UIConstants.screenPaddingLarge, + ), + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header Card + _buildHeaderCard(colorScheme), + + const SizedBox(height: UIConstants.spacing24), + + // Details Section + _buildDetailsSection(colorScheme), + + const SizedBox(height: UIConstants.spacing32), + + // Action Buttons + _buildActionButtons(colorScheme), + ], + ), + ), + ); + }, + ), + ); + } + + Widget _buildHeaderCard(ColorScheme colorScheme) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primary, + colorScheme.primary.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(UIConstants.radius20), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + spreadRadius: 2, + ), + ], + ), + child: Padding( + padding: UIConstants.cardPaddingLarge, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Avatar and Title + Row( + children: [ + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: colorScheme.onPrimary.withOpacity(0.2), + borderRadius: BorderRadius.circular(UIConstants.radius16), + ), + child: Icon( + Icons.person_rounded, + color: colorScheme.onPrimary, + size: 30, + ), + ), + const SizedBox(width: UIConstants.spacing16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _getPrimaryFieldValue(), + style: TextStyle( + color: colorScheme.onPrimary, + fontSize: 24, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: UIConstants.spacing4), + Text( + widget.title, + style: TextStyle( + color: colorScheme.onPrimary.withOpacity(0.8), + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: UIConstants.spacing16), + + // Status Badge + Container( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.spacing12, + vertical: UIConstants.spacing6, + ), + decoration: BoxDecoration( + color: colorScheme.onPrimary.withOpacity(0.2), + borderRadius: BorderRadius.circular(UIConstants.radius12), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration( + color: colorScheme.onPrimary, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: UIConstants.spacing8), + Text( + 'Active', + style: TextStyle( + color: colorScheme.onPrimary, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildDetailsSection(ColorScheme colorScheme) { + final displayFields = widget.displayFields.isNotEmpty + ? widget.displayFields + : _getDefaultDisplayFields(); + + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.surface, + colorScheme.surface.withOpacity(0.95), + ], + ), + borderRadius: BorderRadius.circular(UIConstants.radius20), + border: Border.all( + color: colorScheme.primary.withOpacity(0.1), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 2, + ), + ], + ), + child: Padding( + padding: UIConstants.cardPaddingLarge, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Details', + style: TextStyle( + color: colorScheme.onSurface, + fontSize: 20, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: UIConstants.spacing20), + + // Field Details + ...displayFields + .map((field) => _buildFieldDetail(field, colorScheme)), + ], + ), + ), + ); + } + + Widget _buildFieldDetail( + Map field, ColorScheme colorScheme) { + final value = widget.entity[field['key']]?.toString() ?? ''; + if (value.isEmpty) return const SizedBox.shrink(); + + return Padding( + padding: const EdgeInsets.only(bottom: UIConstants.spacing16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + field['label'] ?? field['key'], + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.7), + fontSize: 12, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: UIConstants.spacing4), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.spacing16, + vertical: UIConstants.spacing12, + ), + decoration: BoxDecoration( + color: _getFieldBackgroundColor(field['type'], colorScheme), + borderRadius: BorderRadius.circular(UIConstants.radius12), + border: Border.all( + color: _getFieldBorderColor(field['type'], colorScheme), + width: 1, + ), + ), + child: Row( + children: [ + Icon( + _getFieldIcon(field['type']), + size: 18, + color: _getFieldIconColor(field['type'], colorScheme), + ), + const SizedBox(width: UIConstants.spacing12), + Expanded( + child: Text( + value, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildActionButtons(ColorScheme colorScheme) { + return Row( + children: [ + Expanded( + child: ModernButton( + text: 'Edit', + type: ModernButtonType.secondary, + size: ModernButtonSize.large, + onPressed: () => widget.onEdit(widget.entity), + icon: Icon(Icons.edit_rounded), + ), + ), + const SizedBox(width: UIConstants.spacing16), + Expanded( + child: ModernButton( + text: 'Delete', + type: ModernButtonType.danger, + size: ModernButtonSize.large, + onPressed: () => _showDeleteDialog(colorScheme), + icon: Icon(Icons.delete_rounded), + ), + ), + ], + ); + } + + void _showDeleteDialog(ColorScheme colorScheme) { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UIConstants.radius20), + ), + title: Text( + 'Delete ${widget.title}', + style: TextStyle( + color: colorScheme.onSurface, + fontWeight: FontWeight.w700, + ), + ), + content: Text( + 'Are you sure you want to delete this ${widget.title.toLowerCase()}? This action cannot be undone.', + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.8), + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + 'Cancel', + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.7), + fontWeight: FontWeight.w600, + ), + ), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + widget.onDelete(widget.entity); + }, + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.error, + foregroundColor: colorScheme.onError, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UIConstants.radius12), + ), + ), + child: const Text( + 'Delete', + style: TextStyle(fontWeight: FontWeight.w600), + ), + ), + ], + ), + ); + } + + String _getPrimaryFieldValue() { + // Try to get the primary field value (usually 'name' or first field) + final primaryFields = ['name', 'title', 'id']; + for (final field in primaryFields) { + final value = widget.entity[field]?.toString(); + if (value != null && value.isNotEmpty) { + return value; + } + } + + // If no primary field found, return first non-empty field + for (final entry in widget.entity.entries) { + if (entry.value != null && entry.value.toString().isNotEmpty) { + return entry.value.toString(); + } + } + + return 'Unknown'; + } + + List> _getDefaultDisplayFields() { + return widget.entity.entries + .where( + (entry) => entry.value != null && entry.value.toString().isNotEmpty) + .map((entry) => { + 'key': entry.key, + 'label': _formatFieldLabel(entry.key), + 'type': _getFieldType(entry.value), + }) + .toList(); + } + + String _formatFieldLabel(String key) { + return key + .split('_') + .map((word) => word[0].toUpperCase() + word.substring(1)) + .join(' '); + } + + String _getFieldType(dynamic value) { + if (value is num) return 'number'; + if (value.toString().contains('@')) return 'email'; + if (RegExp(r'^\+?[\d\s\-\(\)]+$').hasMatch(value.toString())) + return 'phone'; + return 'text'; + } + + Color _getFieldBackgroundColor(String type, ColorScheme colorScheme) { + switch (type) { + case 'phone': + return colorScheme.primaryContainer.withOpacity(0.2); + case 'email': + return colorScheme.secondaryContainer.withOpacity(0.2); + case 'number': + return colorScheme.tertiaryContainer.withOpacity(0.2); + default: + return colorScheme.surfaceVariant.withOpacity(0.3); + } + } + + Color _getFieldBorderColor(String type, ColorScheme colorScheme) { + switch (type) { + case 'phone': + return colorScheme.primary.withOpacity(0.1); + case 'email': + return colorScheme.secondary.withOpacity(0.1); + case 'number': + return colorScheme.tertiary.withOpacity(0.1); + default: + return colorScheme.outline.withOpacity(0.2); + } + } + + IconData _getFieldIcon(String type) { + switch (type) { + case 'phone': + return Icons.phone_rounded; + case 'email': + return Icons.email_rounded; + case 'number': + return Icons.numbers_rounded; + default: + return Icons.text_fields_rounded; + } + } + + Color _getFieldIconColor(String type, ColorScheme colorScheme) { + switch (type) { + case 'phone': + return colorScheme.primary; + case 'email': + return colorScheme.secondary; + case 'number': + return colorScheme.tertiary; + default: + return colorScheme.onSurface.withOpacity(0.7); + } + } +} diff --git a/base_project/lib/BuilderField/shared/ui/entity_form.dart b/base_project/lib/BuilderField/shared/ui/entity_form.dart new file mode 100644 index 0000000..ec8c32e --- /dev/null +++ b/base_project/lib/BuilderField/shared/ui/entity_form.dart @@ -0,0 +1,146 @@ +import 'package:base_project/core/providers/dynamic_theme_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../fields/base_field.dart'; +import '../../../shared/widgets/buttons/modern_button.dart'; +import '../../../core/constants/ui_constants.dart'; + +/// Reusable form component that dynamically renders fields based on field definitions +/// This allows UI to be independent of field types and enables reusability +class EntityForm extends StatefulWidget { + final List fields; + final Map? initialData; + final Function(Map) onSubmit; + final String submitButtonText; + final bool isLoading; + + const EntityForm({ + super.key, + required this.fields, + this.initialData, + required this.onSubmit, + this.submitButtonText = 'Submit', + this.isLoading = false, + }); + + @override + State createState() => _EntityFormState(); +} + +class _EntityFormState extends State { + final _formKey = GlobalKey(); + final Map _controllers = {}; + final Map _fieldByKey = {}; + + @override + void initState() { + super.initState(); + _initializeControllers(); + } + + void _initializeControllers() { + for (final field in widget.fields) { + _controllers[field.fieldKey] = TextEditingController( + text: widget.initialData?[field.fieldKey]?.toString() ?? '', + ); + _fieldByKey[field.fieldKey] = field; + } + } + + @override + void dispose() { + for (final controller in _controllers.values) { + controller.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, dynamicThemeProvider, child) { + final colorScheme = dynamicThemeProvider.getCurrentColorScheme( + Theme.of(context).brightness == Brightness.dark, + ); + + return Form( + key: _formKey, + child: Column( + children: [ + // Dynamic field rendering + ...widget.fields.map((field) => Padding( + padding: + const EdgeInsets.only(bottom: UIConstants.spacing16), + child: field.buildField( + controller: _controllers[field.fieldKey]!, + colorScheme: colorScheme, + onChanged: () => setState(() {}), + ), + )), + + const SizedBox(height: UIConstants.spacing24), + + // Submit button + ModernButton( + text: widget.submitButtonText, + type: ModernButtonType.primary, + size: ModernButtonSize.large, + isLoading: widget.isLoading, + onPressed: widget.isLoading ? null : _handleSubmit, + ), + ], + ), + ); + }, + ); + } + + void _handleSubmit() { + if (_formKey.currentState!.validate()) { + // Dynamic cross-field match for any password-confirm group + final Map passwordByGroup = {}; + final Map confirmByGroup = {}; + for (final entry in _controllers.entries) { + final key = entry.key; + final field = _fieldByKey[key]; + final props = field?.customProperties ?? const {}; + final isPassword = props['isPassword'] == true; + if (!isPassword) continue; + final String? groupId = props['groupId']; + if (groupId == null) continue; + final bool isConfirm = props['isConfirm'] == true; + if (isConfirm) { + confirmByGroup[groupId] = entry.value.text; + } else { + passwordByGroup[groupId] = entry.value.text; + } + } + for (final gid in confirmByGroup.keys) { + final confirm = confirmByGroup[gid] ?? ''; + final pass = passwordByGroup[gid] ?? ''; + if (confirm.isNotEmpty && confirm != pass) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Passwords do not match')), + ); + return; + } + } + + final formData = {}; + for (final entry in _controllers.entries) { + final key = entry.key; + final field = _fieldByKey[key]; + final value = entry.value.text.trim(); + + // Skip confirm entries for any password group + final props = field?.customProperties ?? const {}; + final bool isPassword = props['isPassword'] == true; + final bool isConfirm = props['isConfirm'] == true; + if (isPassword && isConfirm) continue; + + formData[key] = value; + } + widget.onSubmit(formData); + } + } +} diff --git a/base_project/lib/BuilderField/shared/ui/entity_list.dart b/base_project/lib/BuilderField/shared/ui/entity_list.dart new file mode 100644 index 0000000..69a1420 --- /dev/null +++ b/base_project/lib/BuilderField/shared/ui/entity_list.dart @@ -0,0 +1,476 @@ +import 'package:base_project/core/providers/dynamic_theme_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../core/constants/ui_constants.dart'; +import '../../../shared/widgets/app_bar/modern_app_bar.dart'; +import 'entity_card.dart'; + +/// Generic list component for displaying entities with search, pagination, and refresh +/// This component works with any entity type and provides consistent UI/UX +class EntityList extends StatefulWidget { + final List> entities; + final bool isLoading; + final String? errorMessage; + final bool hasMoreData; + final String searchQuery; + final Function(String) onSearchChanged; + final Function(Map) onEdit; + final Function(Map) onDelete; + final Function(Map)? onTap; + final Function() onRefresh; + final Function() onLoadMore; + final String title; + final Function()? onAddNew; + final List> displayFields; + + const EntityList({ + super.key, + required this.entities, + required this.isLoading, + this.errorMessage, + required this.hasMoreData, + required this.searchQuery, + required this.onSearchChanged, + required this.onEdit, + required this.onDelete, + this.onTap, + required this.onRefresh, + required this.onLoadMore, + required this.title, + this.onAddNew, + this.displayFields = const [], + }); + + @override + State createState() => _EntityListState(); +} + +class _EntityListState extends State with TickerProviderStateMixin { + late AnimationController _animationController; + final TextEditingController _searchController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _initializeAnimations(); + _searchController.text = widget.searchQuery; + _scrollController.addListener(_scrollListener); + } + + void _initializeAnimations() { + _animationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _animationController.forward(); + } + + void _scrollListener() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 200) { + if (widget.hasMoreData && !widget.isLoading) { + widget.onLoadMore(); + } + } + } + + @override + void dispose() { + _animationController.dispose(); + _searchController.dispose(); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, dynamicThemeProvider, child) { + final colorScheme = dynamicThemeProvider.getCurrentColorScheme( + Theme.of(context).brightness == Brightness.dark, + ); + + return Scaffold( + backgroundColor: colorScheme.surface, + appBar: ModernAppBar( + title: widget.title, + showBackButton: true, + actions: [ + if (widget.onAddNew != null) + IconButton( + onPressed: widget.onAddNew, + icon: Icon( + Icons.add_rounded, + color: colorScheme.primary, + ), + tooltip: 'Add New', + ), + ], + ), + body: Column( + children: [ + // Search Bar + _buildSearchBar(colorScheme), + + // Content + Expanded( + child: _buildContent(colorScheme), + ), + ], + ), + ); + }, + ); + } + + Widget _buildSearchBar(ColorScheme colorScheme) { + return Container( + margin: UIConstants.getResponsivePadding( + context, + mobile: UIConstants.screenPaddingMedium, + tablet: UIConstants.screenPaddingLarge, + desktop: UIConstants.screenPaddingLarge, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.surface, + colorScheme.surface.withOpacity(0.95), + ], + ), + borderRadius: BorderRadius.circular(UIConstants.radius16), + border: Border.all( + color: colorScheme.primary.withOpacity(0.1), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(0.08), + blurRadius: 15, + offset: const Offset(0, 5), + spreadRadius: 1, + ), + ], + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: _searchController, + onChanged: widget.onSearchChanged, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: 16, + fontWeight: FontWeight.w500, + ), + decoration: InputDecoration( + hintText: 'Search ${widget.title.toLowerCase()}...', + hintStyle: TextStyle( + color: colorScheme.onSurface.withOpacity(0.6), + fontSize: 16, + fontWeight: FontWeight.w400, + ), + prefixIcon: Icon( + Icons.search_rounded, + color: colorScheme.primary, + size: 22, + ), + suffixIcon: _searchController.text.isNotEmpty + ? IconButton( + onPressed: () { + _searchController.clear(); + widget.onSearchChanged(''); + }, + icon: Icon( + Icons.clear_rounded, + color: colorScheme.onSurface.withOpacity(0.6), + size: 20, + ), + ) + : null, + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: UIConstants.spacing16, + vertical: UIConstants.spacing12, + ), + ), + ), + ), + IconButton( + onPressed: widget.onRefresh, + icon: Icon( + Icons.refresh_rounded, + color: colorScheme.onSurface.withOpacity(0.8), + size: 22, + ), + tooltip: 'Refresh', + ), + ], + ), + ); + } + + Widget _buildContent(ColorScheme colorScheme) { + if (widget.isLoading && widget.entities.isEmpty) { + return _buildLoadingState(colorScheme); + } + + if (widget.errorMessage != null && widget.entities.isEmpty) { + return _buildErrorState(colorScheme); + } + + if (widget.entities.isEmpty) { + return _buildEmptyState(colorScheme); + } + + return _buildEntityGrid(colorScheme); + } + + Widget _buildLoadingState(ColorScheme colorScheme) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: colorScheme.primary, + strokeWidth: 3, + ), + const SizedBox(height: UIConstants.spacing16), + Text( + 'Loading ${widget.title.toLowerCase()}...', + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.7), + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildErrorState(ColorScheme colorScheme) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: colorScheme.errorContainer.withOpacity(0.2), + borderRadius: BorderRadius.circular(UIConstants.radius20), + ), + child: Icon( + Icons.error_outline_rounded, + color: colorScheme.error, + size: 40, + ), + ), + const SizedBox(height: UIConstants.spacing16), + Text( + 'Error Loading Data', + style: TextStyle( + color: colorScheme.onSurface, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: UIConstants.spacing8), + Text( + widget.errorMessage ?? 'Something went wrong', + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.7), + fontSize: 14, + fontWeight: FontWeight.w400, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: UIConstants.spacing24), + ElevatedButton.icon( + onPressed: widget.onRefresh, + icon: Icon( + Icons.refresh_rounded, + color: colorScheme.onPrimary, + ), + label: Text( + 'Retry', + style: TextStyle( + color: colorScheme.onPrimary, + fontWeight: FontWeight.w600, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + elevation: 4, + shadowColor: colorScheme.primary.withOpacity(0.3), + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.spacing24, + vertical: UIConstants.spacing12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UIConstants.radius12), + ), + ), + ), + ], + ), + ); + } + + Widget _buildEmptyState(ColorScheme colorScheme) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: colorScheme.primaryContainer.withOpacity(0.2), + borderRadius: BorderRadius.circular(UIConstants.radius20), + ), + child: Icon( + Icons.inbox_outlined, + color: colorScheme.primary, + size: 40, + ), + ), + const SizedBox(height: UIConstants.spacing16), + Text( + 'No ${widget.title.toLowerCase()} found', + style: TextStyle( + color: colorScheme.onSurface, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: UIConstants.spacing8), + Text( + widget.searchQuery.isNotEmpty + ? 'Try adjusting your search terms' + : 'Start by adding your first ${widget.title.toLowerCase()}', + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.7), + fontSize: 14, + fontWeight: FontWeight.w400, + ), + textAlign: TextAlign.center, + ), + if (widget.onAddNew != null) ...[ + const SizedBox(height: UIConstants.spacing24), + ElevatedButton.icon( + onPressed: widget.onAddNew, + icon: Icon( + Icons.add_rounded, + color: colorScheme.onPrimary, + ), + label: Text( + 'Add ${widget.title}', + style: TextStyle( + color: colorScheme.onPrimary, + fontWeight: FontWeight.w600, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: colorScheme.onPrimary, + elevation: 4, + shadowColor: colorScheme.primary.withOpacity(0.3), + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.spacing24, + vertical: UIConstants.spacing12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UIConstants.radius12), + ), + ), + ), + ], + ], + ), + ); + } + + Widget _buildEntityGrid(ColorScheme colorScheme) { + return RefreshIndicator( + onRefresh: () async => widget.onRefresh(), + color: colorScheme.primary, + child: GridView.builder( + controller: _scrollController, + padding: UIConstants.getResponsivePadding( + context, + mobile: UIConstants.screenPaddingMedium, + tablet: UIConstants.screenPaddingLarge, + desktop: UIConstants.screenPaddingLarge, + ), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: UIConstants.getResponsiveInt( + context, + mobile: 1, + tablet: 2, + desktop: 3, + ), + childAspectRatio: 0.85, + crossAxisSpacing: UIConstants.spacing16, + mainAxisSpacing: UIConstants.spacing16, + ), + itemCount: widget.entities.length + (widget.isLoading ? 1 : 0), + itemBuilder: (context, index) { + if (index < widget.entities.length) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return FadeTransition( + opacity: _animationController, + child: Transform.translate( + offset: Offset(0, 20 * (1 - _animationController.value)), + child: EntityCard( + entity: widget.entities[index], + onEdit: widget.onEdit, + onDelete: widget.onDelete, + onTap: widget.onTap, + displayFields: widget.displayFields, + ), + ), + ); + }, + ); + } else { + return _buildLoadingCard(colorScheme); + } + }, + ), + ); + } + + Widget _buildLoadingCard(ColorScheme colorScheme) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.surface, + colorScheme.surface.withOpacity(0.95), + ], + ), + borderRadius: BorderRadius.circular(UIConstants.radius20), + border: Border.all( + color: colorScheme.primary.withOpacity(0.1), + width: 1.5, + ), + ), + child: Center( + child: CircularProgressIndicator( + color: colorScheme.primary, + strokeWidth: 2, + ), + ), + ); + } +} diff --git a/base_project/lib/BuilderField/shared/ui/entity_screens.dart b/base_project/lib/BuilderField/shared/ui/entity_screens.dart new file mode 100644 index 0000000..26163a5 --- /dev/null +++ b/base_project/lib/BuilderField/shared/ui/entity_screens.dart @@ -0,0 +1,608 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../../../core/providers/dynamic_theme_provider.dart'; +import '../../../core/constants/ui_constants.dart'; +import '../../../shared/widgets/app_bar/modern_app_bar.dart'; +import '../../../shared/widgets/buttons/modern_button.dart'; +import '../fields/base_field.dart'; +import 'entity_form.dart'; + +/// Generic CRUD screens for entities +/// This provides consistent create and update screens for all entity types + +/// Generic Create Entity Screen +class EntityCreateScreen extends StatefulWidget { + final List fields; + final Function(Map) onSubmit; + final String title; + final bool isLoading; + final String? errorMessage; + + const EntityCreateScreen({ + super.key, + required this.fields, + required this.onSubmit, + required this.title, + this.isLoading = false, + this.errorMessage, + }); + + @override + State createState() => _EntityCreateScreenState(); +} + +class _EntityCreateScreenState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + _initializeAnimations(); + } + + void _initializeAnimations() { + _animationController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0.0, 0.6, curve: Curves.easeOut), + )); + + _slideAnimation = Tween( + begin: const Offset(0, 0.3), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0.2, 1.0, curve: Curves.easeOutCubic), + )); + + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, dynamicThemeProvider, child) { + final colorScheme = dynamicThemeProvider.getCurrentColorScheme( + Theme.of(context).brightness == Brightness.dark, + ); + + return Scaffold( + backgroundColor: colorScheme.surface, + appBar: ModernAppBar( + title: 'Create ${widget.title}', + showBackButton: true, + ), + body: widget.isLoading + ? _buildLoadingState(colorScheme) + : _buildContent(colorScheme), + ); + }, + ); + } + + Widget _buildLoadingState(ColorScheme colorScheme) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: colorScheme.primary, + strokeWidth: 3, + ), + const SizedBox(height: UIConstants.spacing16), + Text( + 'Creating ${widget.title.toLowerCase()}...', + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.7), + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildContent(ColorScheme colorScheme) { + return SingleChildScrollView( + padding: UIConstants.getResponsivePadding( + context, + mobile: UIConstants.screenPaddingMedium, + tablet: UIConstants.screenPaddingLarge, + desktop: UIConstants.screenPaddingLarge, + ), + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + _buildHeader(colorScheme), + + const SizedBox(height: UIConstants.spacing24), + + // Form + _buildForm(colorScheme), + + if (widget.errorMessage != null) ...[ + const SizedBox(height: UIConstants.spacing16), + _buildErrorMessage(colorScheme), + ], + ], + ), + ), + ); + }, + ), + ); + } + + Widget _buildHeader(ColorScheme colorScheme) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.primary, + colorScheme.primary.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(UIConstants.radius20), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + spreadRadius: 2, + ), + ], + ), + child: Padding( + padding: UIConstants.cardPaddingLarge, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: colorScheme.onPrimary.withOpacity(0.2), + borderRadius: BorderRadius.circular(UIConstants.radius12), + ), + child: Icon( + Icons.add_rounded, + color: colorScheme.onPrimary, + size: 24, + ), + ), + const SizedBox(width: UIConstants.spacing16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Create New ${widget.title}', + style: TextStyle( + color: colorScheme.onPrimary, + fontSize: 20, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: UIConstants.spacing4), + Text( + 'Fill in the details below to create a new ${widget.title.toLowerCase()}', + style: TextStyle( + color: colorScheme.onPrimary.withOpacity(0.8), + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildForm(ColorScheme colorScheme) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.surface, + colorScheme.surface.withOpacity(0.95), + ], + ), + borderRadius: BorderRadius.circular(UIConstants.radius20), + border: Border.all( + color: colorScheme.primary.withOpacity(0.1), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 2, + ), + ], + ), + child: Padding( + padding: UIConstants.cardPaddingLarge, + child: EntityForm( + fields: widget.fields, + onSubmit: widget.onSubmit, + submitButtonText: 'Create ${widget.title}', + isLoading: widget.isLoading, + ), + ), + ); + } + + Widget _buildErrorMessage(ColorScheme colorScheme) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(UIConstants.spacing16), + decoration: BoxDecoration( + color: colorScheme.errorContainer.withOpacity(0.2), + borderRadius: BorderRadius.circular(UIConstants.radius12), + border: Border.all( + color: colorScheme.error.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.error_outline_rounded, + color: colorScheme.error, + size: 20, + ), + const SizedBox(width: UIConstants.spacing12), + Expanded( + child: Text( + widget.errorMessage!, + style: TextStyle( + color: colorScheme.error, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + } +} + +/// Generic Update Entity Screen +class EntityUpdateScreen extends StatefulWidget { + final List fields; + final Map initialData; + final Function(Map) onSubmit; + final String title; + final bool isLoading; + final String? errorMessage; + + const EntityUpdateScreen({ + super.key, + required this.fields, + required this.initialData, + required this.onSubmit, + required this.title, + this.isLoading = false, + this.errorMessage, + }); + + @override + State createState() => _EntityUpdateScreenState(); +} + +class _EntityUpdateScreenState extends State + with TickerProviderStateMixin { + late AnimationController _animationController; + late Animation _fadeAnimation; + late Animation _slideAnimation; + + @override + void initState() { + super.initState(); + _initializeAnimations(); + } + + void _initializeAnimations() { + _animationController = AnimationController( + duration: const Duration(milliseconds: 600), + vsync: this, + ); + + _fadeAnimation = Tween( + begin: 0.0, + end: 1.0, + ).animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0.0, 0.6, curve: Curves.easeOut), + )); + + _slideAnimation = Tween( + begin: const Offset(0, 0.3), + end: Offset.zero, + ).animate(CurvedAnimation( + parent: _animationController, + curve: const Interval(0.2, 1.0, curve: Curves.easeOutCubic), + )); + + _animationController.forward(); + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Consumer( + builder: (context, dynamicThemeProvider, child) { + final colorScheme = dynamicThemeProvider.getCurrentColorScheme( + Theme.of(context).brightness == Brightness.dark, + ); + + return Scaffold( + backgroundColor: colorScheme.surface, + appBar: ModernAppBar( + title: 'Update ${widget.title}', + showBackButton: true, + ), + body: widget.isLoading + ? _buildLoadingState(colorScheme) + : _buildContent(colorScheme), + ); + }, + ); + } + + Widget _buildLoadingState(ColorScheme colorScheme) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + color: colorScheme.primary, + strokeWidth: 3, + ), + const SizedBox(height: UIConstants.spacing16), + Text( + 'Updating ${widget.title.toLowerCase()}...', + style: TextStyle( + color: colorScheme.onSurface.withOpacity(0.7), + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ); + } + + Widget _buildContent(ColorScheme colorScheme) { + return SingleChildScrollView( + padding: UIConstants.getResponsivePadding( + context, + mobile: UIConstants.screenPaddingMedium, + tablet: UIConstants.screenPaddingLarge, + desktop: UIConstants.screenPaddingLarge, + ), + child: AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return FadeTransition( + opacity: _fadeAnimation, + child: SlideTransition( + position: _slideAnimation, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header + _buildHeader(colorScheme), + + const SizedBox(height: UIConstants.spacing24), + + // Form + _buildForm(colorScheme), + + if (widget.errorMessage != null) ...[ + const SizedBox(height: UIConstants.spacing16), + _buildErrorMessage(colorScheme), + ], + ], + ), + ), + ); + }, + ), + ); + } + + Widget _buildHeader(ColorScheme colorScheme) { + return Container( + width: double.infinity, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.secondary, + colorScheme.secondary.withOpacity(0.8), + ], + ), + borderRadius: BorderRadius.circular(UIConstants.radius20), + boxShadow: [ + BoxShadow( + color: colorScheme.secondary.withOpacity(0.3), + blurRadius: 20, + offset: const Offset(0, 10), + spreadRadius: 2, + ), + ], + ), + child: Padding( + padding: UIConstants.cardPaddingLarge, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: colorScheme.onSecondary.withOpacity(0.2), + borderRadius: BorderRadius.circular(UIConstants.radius12), + ), + child: Icon( + Icons.edit_rounded, + color: colorScheme.onSecondary, + size: 24, + ), + ), + const SizedBox(width: UIConstants.spacing16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Update ${widget.title}', + style: TextStyle( + color: colorScheme.onSecondary, + fontSize: 20, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: UIConstants.spacing4), + Text( + 'Modify the details below to update this ${widget.title.toLowerCase()}', + style: TextStyle( + color: colorScheme.onSecondary.withOpacity(0.8), + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildForm(ColorScheme colorScheme) { + return Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colorScheme.surface, + colorScheme.surface.withOpacity(0.95), + ], + ), + borderRadius: BorderRadius.circular(UIConstants.radius20), + border: Border.all( + color: colorScheme.primary.withOpacity(0.1), + width: 1.5, + ), + boxShadow: [ + BoxShadow( + color: colorScheme.primary.withOpacity(0.08), + blurRadius: 20, + offset: const Offset(0, 8), + spreadRadius: 2, + ), + ], + ), + child: Padding( + padding: UIConstants.cardPaddingLarge, + child: EntityForm( + fields: widget.fields, + initialData: widget.initialData, + onSubmit: widget.onSubmit, + submitButtonText: 'Update ${widget.title}', + isLoading: widget.isLoading, + ), + ), + ); + } + + Widget _buildErrorMessage(ColorScheme colorScheme) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(UIConstants.spacing16), + decoration: BoxDecoration( + color: colorScheme.errorContainer.withOpacity(0.2), + borderRadius: BorderRadius.circular(UIConstants.radius12), + border: Border.all( + color: colorScheme.error.withOpacity(0.3), + width: 1, + ), + ), + child: Row( + children: [ + Icon( + Icons.error_outline_rounded, + color: colorScheme.error, + size: 20, + ), + const SizedBox(width: UIConstants.spacing12), + Expanded( + child: Text( + widget.errorMessage!, + style: TextStyle( + color: colorScheme.error, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ); + } +} diff --git a/base_project/lib/BuilderField/shared/utils/entity_field_store.dart b/base_project/lib/BuilderField/shared/utils/entity_field_store.dart new file mode 100644 index 0000000..d7d0124 --- /dev/null +++ b/base_project/lib/BuilderField/shared/utils/entity_field_store.dart @@ -0,0 +1,37 @@ +import 'dart:typed_data'; + +/// Temporary in-memory store for complex field data (e.g., uploads) +/// Keyed by fieldKey. Values are dynamic but expected shapes are documented. +class EntityFieldStore { + EntityFieldStore._(); + static final EntityFieldStore instance = EntityFieldStore._(); + + final Map _data = {}; + + void set(String fieldKey, dynamic value) { + _data[fieldKey] = value; + } + + T? get(String fieldKey) { + final value = _data[fieldKey]; + if (value is T) return value as T; + return null; + } + + void remove(String fieldKey) { + _data.remove(fieldKey); + } + + void clear() { + _data.clear(); + } +} + +/// Expected value shapes: +/// - For multi-file uploads: List +class UploadItem { + final String fileName; + final Uint8List bytes; + + UploadItem({required this.fileName, required this.bytes}); +} diff --git a/base_project/lib/shared/widgets/inputs/modern_text_field.dart b/base_project/lib/shared/widgets/inputs/modern_text_field.dart index b5451ce..006856d 100644 --- a/base_project/lib/shared/widgets/inputs/modern_text_field.dart +++ b/base_project/lib/shared/widgets/inputs/modern_text_field.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import '../../../../../core/constants/ui_constants.dart'; +import '../../../core/constants/ui_constants.dart'; class ModernTextField extends StatefulWidget { final String? label; @@ -71,6 +71,7 @@ class _ModernTextFieldState extends State late Animation _scaleAnimation; bool _isFocused = false; bool _hasError = false; + late bool _obscureText; @override void initState() { @@ -79,6 +80,7 @@ class _ModernTextFieldState extends State duration: UIConstants.durationFast, vsync: this, ); + _obscureText = widget.obscureText; _fadeAnimation = Tween( begin: 0.0, @@ -183,7 +185,7 @@ class _ModernTextFieldState extends State controller: widget.controller, focusNode: widget.focusNode, keyboardType: widget.keyboardType, - obscureText: widget.obscureText, + obscureText: _obscureText, enabled: widget.enabled, readOnly: widget.readOnly, maxLines: widget.maxLines, @@ -229,7 +231,7 @@ class _ModernTextFieldState extends State ), ) : null, - suffixIcon: widget.suffixIcon != null + suffixIcon: (widget.suffixIcon != null) ? Padding( padding: const EdgeInsets.only( right: UIConstants.spacing16), @@ -243,7 +245,20 @@ class _ModernTextFieldState extends State child: widget.suffixIcon!, ), ) - : null, + : (widget.obscureText + ? IconButton( + onPressed: () { + setState(() { + _obscureText = !_obscureText; + }); + }, + icon: Icon( + _obscureText + ? Icons.visibility_off_outlined + : Icons.visibility_outlined, + ), + ) + : null), border: InputBorder.none, contentPadding: widget.contentPadding ?? const EdgeInsets.symmetric(