diff --git a/lib/controllers/details_controller.dart b/lib/controllers/details_controller.dart index a4e63c3..321c5a0 100644 --- a/lib/controllers/details_controller.dart +++ b/lib/controllers/details_controller.dart @@ -1,10 +1,10 @@ import 'package:get/get.dart'; import '../model/filament_model.dart'; import '../helpers/filament_repository.dart'; +import '../pages/edit_view.dart'; class DetailsController extends GetxController { Rx filament = Rx(null); - final isEditing = false.obs; @override void onInit() { @@ -14,19 +14,9 @@ class DetailsController extends GetxController { super.onInit(); } - void toggleEdit() { - isEditing.value = !isEditing.value; - } - - void updateFilament(FilamentModel updatedFilament) { - FilamentRepository.to.updateFilament(updatedFilament); - filament.value = updatedFilament; - isEditing.value = false; - Get.snackbar( - 'Erfolg', - 'Filament wurde aktualisiert', - snackPosition: SnackPosition.BOTTOM, - ); + void onEditPressed() { + // Implement edit functionality here + Get.toNamed(EditPage.namedRoute, arguments: {'filament': filament.value}); } void deleteFilament() { diff --git a/lib/controllers/edit_controller.dart b/lib/controllers/edit_controller.dart new file mode 100644 index 0000000..73cae00 --- /dev/null +++ b/lib/controllers/edit_controller.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../model/filament_model.dart'; +import '../helpers/filament_repository.dart'; + +class EditController extends GetxController { + Rx originalFilament = Rx(null); + + // Form Controllers + final nameController = TextEditingController(); + final typeController = TextEditingController(); + final colorController = TextEditingController(); + final weightController = TextEditingController(); + final weightUsedController = TextEditingController(); + final priceController = TextEditingController(); + final manufacturerController = TextEditingController(); + final purchaseDateController = TextEditingController(); + final notesController = TextEditingController(); + final piecesController = TextEditingController(); + final printingTempController = TextEditingController(); + final bedTempController = TextEditingController(); + + // Validation + final formKey = GlobalKey(); + final isSaving = false.obs; + + // Available options + final List filamentTypes = [ + 'PLA', + 'ABS', + 'PETG', + 'TPU', + 'ASA', + 'Nylon', + 'PC', + 'PVA', + ]; + + final List availableColors = [ + 'White', + 'Black', + 'Red', + 'Blue', + 'Green', + 'Yellow', + 'Orange', + 'Purple', + 'Pink', + 'Grey', + 'Brown', + ]; + + @override + void onInit() { + if (Get.arguments != null && Get.arguments['filament'] != null) { + originalFilament.value = Get.arguments['filament']; + _populateForm(); + } + super.onInit(); + } + + void _populateForm() { + final filament = originalFilament.value; + if (filament != null) { + nameController.text = filament.name; + typeController.text = filament.type; + colorController.text = filament.color; + weightController.text = filament.weight.toString(); + weightUsedController.text = filament.weightUsed.toString(); + priceController.text = filament.price.toString(); + manufacturerController.text = filament.manufacturer ?? ''; + purchaseDateController.text = filament.purchaseDate ?? ''; + notesController.text = filament.notes ?? ''; + piecesController.text = filament.pices?.toString() ?? ''; + printingTempController.text = filament.printingTemp?.toString() ?? ''; + bedTempController.text = filament.bedTemp?.toString() ?? ''; + } + } + + Future saveFilament() async { + if (!formKey.currentState!.validate()) return; + + isSaving.value = true; + + try { + final updatedFilament = FilamentModel( + id: originalFilament.value?.id ?? DateTime.now().millisecondsSinceEpoch.toString(), + name: nameController.text.trim(), + type: typeController.text.trim(), + color: colorController.text.trim(), + weight: double.tryParse(weightController.text) ?? 0, + weightUsed: double.tryParse(weightUsedController.text) ?? 0, + price: double.tryParse(priceController.text) ?? 0, + manufacturer: manufacturerController.text.trim().isEmpty + ? null + : manufacturerController.text.trim(), + purchaseDate: purchaseDateController.text.trim().isEmpty + ? null + : purchaseDateController.text.trim(), + notes: notesController.text.trim().isEmpty + ? null + : notesController.text.trim(), + pices: int.tryParse(piecesController.text), + printingTemp: int.tryParse(printingTempController.text), + bedTemp: int.tryParse(bedTempController.text), + ); + + bool success; + if (originalFilament.value != null) { + success = await FilamentRepository.to.updateFilament(updatedFilament); + } else { + success = await FilamentRepository.to.createFilament(updatedFilament); + } + + if (success) { + Get.back(result: updatedFilament); + Get.snackbar( + 'Erfolg', + originalFilament.value != null + ? 'Filament wurde aktualisiert' + : 'Filament wurde erstellt', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Get.theme.colorScheme.primary.withAlpha(26), + colorText: Get.theme.colorScheme.primary, + ); + } else { + Get.snackbar( + 'Fehler', + 'Filament konnte nicht gespeichert werden', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red.withAlpha(26), + colorText: Colors.red, + ); + } + } catch (e) { + Get.snackbar( + 'Fehler', + 'Ein Fehler ist aufgetreten: $e', + snackPosition: SnackPosition.BOTTOM, + backgroundColor: Colors.red.withAlpha(26), + colorText: Colors.red, + ); + } finally { + isSaving.value = false; + } + } + + @override + void onClose() { + nameController.dispose(); + typeController.dispose(); + colorController.dispose(); + weightController.dispose(); + weightUsedController.dispose(); + priceController.dispose(); + manufacturerController.dispose(); + purchaseDateController.dispose(); + notesController.dispose(); + piecesController.dispose(); + printingTempController.dispose(); + bedTempController.dispose(); + super.onClose(); + } +} diff --git a/lib/controllers/list_controller.dart b/lib/controllers/list_controller.dart index eb3e986..9fd877d 100644 --- a/lib/controllers/list_controller.dart +++ b/lib/controllers/list_controller.dart @@ -2,6 +2,7 @@ import 'package:get/get.dart'; import '../../model/filament_model.dart'; import '../../helpers/filament_repository.dart'; import '../pages/details_view.dart'; +import '../pages/edit_view.dart'; class ListController extends GetxController { final filamentList = [].obs; @@ -35,7 +36,9 @@ class ListController extends GetxController { Get.toNamed(DetailsPage.namedRoute, arguments: {'filament': filament}); } - void editFilament(FilamentModel filament) {} + void editFilament(FilamentModel filament) { + Get.toNamed(EditPage.namedRoute, arguments: {'filament': filament}); + } void removeFilament(FilamentModel filament) { FilamentRepository.to.deleteFilament(filament.id); diff --git a/lib/helpers/sample_bindings.dart b/lib/helpers/sample_bindings.dart index 0f7fcfb..233f8a2 100644 --- a/lib/helpers/sample_bindings.dart +++ b/lib/helpers/sample_bindings.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import '../controllers/details_controller.dart'; +import '../controllers/edit_controller.dart'; import '../controllers/home_controller.dart'; import '../controllers/list_controller.dart'; @@ -13,6 +14,7 @@ class SampleBindings extends Bindings { Get.lazyPut(() => HomeController()); Get.lazyPut(() => ListController()); Get.lazyPut(() => DetailsController()); + Get.lazyPut(() => EditController()); } } \ No newline at end of file diff --git a/lib/helpers/sample_routes.dart b/lib/helpers/sample_routes.dart index 858eac4..9d00977 100644 --- a/lib/helpers/sample_routes.dart +++ b/lib/helpers/sample_routes.dart @@ -1,4 +1,5 @@ import 'package:get/get.dart'; +import '../pages/edit_view.dart'; import 'sample_bindings.dart'; import '../pages/home_view.dart'; import '../pages/list_view.dart'; @@ -25,6 +26,11 @@ class SampleRouts { page: () => const DetailsPage(), binding: sampleBindings, ), + GetPage( + name: EditPage.namedRoute, + page: () => const EditPage(), + binding: sampleBindings, + ), ]; diff --git a/lib/pages/details_view.dart b/lib/pages/details_view.dart index bbb3f84..d6946c9 100644 --- a/lib/pages/details_view.dart +++ b/lib/pages/details_view.dart @@ -319,7 +319,7 @@ class DetailsPage extends GetView { icon: Icons.edit, label: 'Bearbeiten', color: Colors.blue, - onPressed: controller.toggleEdit, + onPressed: () => controller.onEditPressed(), ), ), SizedBox(width: 12), diff --git a/lib/pages/edit_view.dart b/lib/pages/edit_view.dart new file mode 100644 index 0000000..27e3b93 --- /dev/null +++ b/lib/pages/edit_view.dart @@ -0,0 +1,409 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:intl/intl.dart'; +import '../controllers/edit_controller.dart'; +import '../widgets/custom_text_field.dart'; +import '../widgets/custom_dropdown.dart'; +import '../widgets/section_header.dart'; +import '../widgets/color_selector.dart'; +import '../widgets/action_button.dart'; + +class EditPage extends GetView { + static const String namedRoute = '/edit-page'; + const EditPage({super.key}); + + @override + Widget build(BuildContext context) { + final isNewFilament = controller.originalFilament.value == null; + + return Scaffold( + backgroundColor: Colors.grey.shade50, + appBar: AppBar( + title: Text( + isNewFilament ? 'Neues Filament' : 'Filament bearbeiten', + style: TextStyle(fontWeight: FontWeight.bold), + ), + backgroundColor: Colors.white, + elevation: 0, + centerTitle: true, + leading: IconButton( + icon: Icon(Icons.close), + onPressed: () => Get.back(), + ), + ), + body: Form( + key: controller.formKey, + child: SingleChildScrollView( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Header Card + Container( + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue.shade400, Colors.blue.shade600], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: Colors.blue.withAlpha(77), + blurRadius: 15, + offset: Offset(0, 5), + ), + ], + ), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withAlpha(51), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + isNewFilament ? Icons.add_circle : Icons.edit, + color: Colors.white, + size: 32, + ), + ), + SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + isNewFilament ? 'Neues Filament anlegen' : 'Filament bearbeiten', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + SizedBox(height: 4), + Text( + isNewFilament + ? 'Fülle die Felder aus, um ein neues Filament hinzuzufügen' + : 'Aktualisiere die Informationen deines Filaments', + style: TextStyle( + fontSize: 13, + color: Colors.white.withAlpha(230), + ), + ), + ], + ), + ), + ], + ), + ), + + SizedBox(height: 24), + + // Basic Information + SectionHeader( + title: 'Grundinformationen', + icon: Icons.info_outline, + color: Colors.blue, + ), + SizedBox(height: 16), + + CustomTextField( + controller: controller.nameController, + label: 'Name *', + hint: 'z.B. 3Djake ECO Filament', + icon: Icons.label, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Bitte einen Namen eingeben'; + } + return null; + }, + ), + + SizedBox(height: 16), + + CustomDropdown( + value: controller.typeController.text, + label: 'Typ *', + icon: Icons.category, + items: controller.filamentTypes, + onChanged: (value) { + if (value != null) { + controller.typeController.text = value; + } + }, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Bitte einen Typ auswählen'; + } + return null; + }, + ), + + SizedBox(height: 16), + + ColorSelector( + selectedColor: controller.colorController.text.isNotEmpty + ? controller.colorController.text + : 'White', + colors: controller.availableColors, + onColorSelected: (color) { + controller.colorController.text = color; + }, + ), + + SizedBox(height: 24), + + // Weight & Price + SectionHeader( + title: 'Gewicht & Preis', + icon: Icons.shopping_cart, + color: Colors.green, + ), + SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: CustomTextField( + controller: controller.weightController, + label: 'Gesamtgewicht (g) *', + hint: '1000', + icon: Icons.scale, + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Erforderlich'; + } + if (double.tryParse(value) == null) { + return 'Ungültige Zahl'; + } + return null; + }, + ), + ), + SizedBox(width: 12), + Expanded( + child: CustomTextField( + controller: controller.weightUsedController, + label: 'Verbraucht (g)', + hint: '0', + icon: Icons.trending_down, + keyboardType: TextInputType.number, + validator: (value) { + if (value != null && value.isNotEmpty) { + if (double.tryParse(value) == null) { + return 'Ungültig'; + } + } + return null; + }, + ), + ), + ], + ), + + SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: CustomTextField( + controller: controller.priceController, + label: 'Preis (€) *', + hint: '19.99', + icon: Icons.euro, + keyboardType: TextInputType.number, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Erforderlich'; + } + if (double.tryParse(value) == null) { + return 'Ungültige Zahl'; + } + return null; + }, + ), + ), + SizedBox(width: 12), + Expanded( + child: CustomTextField( + controller: controller.piecesController, + label: 'Anzahl Rollen', + hint: '1', + icon: Icons.inventory_2, + keyboardType: TextInputType.number, + ), + ), + ], + ), + + SizedBox(height: 24), + + // Print Settings + SectionHeader( + title: 'Druckeinstellungen', + icon: Icons.print, + color: Colors.orange, + ), + SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: CustomTextField( + controller: controller.printingTempController, + label: 'Drucktemperatur (°C)', + hint: '200', + icon: Icons.thermostat, + keyboardType: TextInputType.number, + ), + ), + SizedBox(width: 12), + Expanded( + child: CustomTextField( + controller: controller.bedTempController, + label: 'Betttemperatur (°C)', + hint: '60', + icon: Icons.heat_pump, + keyboardType: TextInputType.number, + ), + ), + ], + ), + + SizedBox(height: 24), + + // Additional Info + SectionHeader( + title: 'Zusatzinformationen', + icon: Icons.more_horiz, + color: Colors.purple, + ), + SizedBox(height: 16), + + CustomTextField( + controller: controller.manufacturerController, + label: 'Hersteller', + hint: 'z.B. 3Djake.at', + icon: Icons.business, + ), + + SizedBox(height: 16), + + CustomTextField( + controller: controller.purchaseDateController, + label: 'Kaufdatum', + hint: 'TT.MM.JJJJ', + icon: Icons.calendar_today, + readOnly: true, + onTap: () async { + final date = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2020), + lastDate: DateTime.now(), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: Colors.blue, + ), + ), + child: child!, + ); + }, + ); + if (date != null) { + final formatter = DateFormat('dd.MM.yyyy'); + controller.purchaseDateController.text = formatter.format(date); + } + }, + suffix: Icon(Icons.arrow_drop_down, color: Colors.blue.shade400), + ), + + SizedBox(height: 16), + + CustomTextField( + controller: controller.notesController, + label: 'Notizen', + hint: 'Zusätzliche Informationen...', + icon: Icons.notes, + maxLines: 4, + ), + + SizedBox(height: 32), + + // Save Button + Obx(() => AnimatedContainer( + duration: Duration(milliseconds: 300), + height: 56, + child: controller.isSaving.value + ? Container( + decoration: BoxDecoration( + color: Colors.blue.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation( + Colors.blue, + ), + ), + ), + SizedBox(width: 12), + Text( + 'Wird gespeichert...', + style: TextStyle( + color: Colors.blue, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ) + : ActionButton( + icon: Icons.save, + label: isNewFilament ? 'Erstellen' : 'Speichern', + color: Colors.blue, + onPressed: controller.saveFilament, + ), + )), + + SizedBox(height: 16), + + // Cancel Button + TextButton( + onPressed: () => Get.back(), + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric(vertical: 14), + ), + child: Text( + 'Abbrechen', + style: TextStyle( + fontSize: 15, + color: Colors.grey.shade600, + fontWeight: FontWeight.w600, + ), + ), + ), + + SizedBox(height: 32), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/color_selector.dart b/lib/widgets/color_selector.dart new file mode 100644 index 0000000..0d946bd --- /dev/null +++ b/lib/widgets/color_selector.dart @@ -0,0 +1,103 @@ +import 'package:flutter/material.dart'; + +class ColorSelector extends StatelessWidget { + final String selectedColor; + final List colors; + final void Function(String) onColorSelected; + + const ColorSelector({ + super.key, + required this.selectedColor, + required this.colors, + required this.onColorSelected, + }); + + Color _getColorFromString(String colorName) { + final colorMap = { + 'red': Colors.red, + 'blue': Colors.blue, + 'green': Colors.green, + 'yellow': Colors.yellow, + 'black': Colors.black, + 'white': Colors.white, + 'orange': Colors.orange, + 'purple': Colors.purple, + 'pink': Colors.pink, + 'grey': Colors.grey, + 'brown': Colors.brown, + }; + return colorMap[colorName.toLowerCase()] ?? Colors.grey; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Farbe', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + SizedBox(height: 12), + Wrap( + spacing: 12, + runSpacing: 12, + children: colors.map((colorName) { + final isSelected = selectedColor.toLowerCase() == colorName.toLowerCase(); + final color = _getColorFromString(colorName); + + return GestureDetector( + onTap: () => onColorSelected(colorName), + child: AnimatedContainer( + duration: Duration(milliseconds: 200), + width: isSelected ? 60 : 50, + height: isSelected ? 60 : 50, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? Colors.blue : Colors.grey.shade300, + width: isSelected ? 3 : 2, + ), + boxShadow: [ + if (isSelected) + BoxShadow( + color: color.withAlpha(102), + blurRadius: 12, + spreadRadius: 2, + ), + ], + ), + child: isSelected + ? Icon( + Icons.check, + color: _isLightColor(color) ? Colors.black : Colors.white, + size: 28, + ) + : null, + ), + ); + }).toList(), + ), + SizedBox(height: 8), + Text( + 'Ausgewählt: $selectedColor', + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + fontStyle: FontStyle.italic, + ), + ), + ], + ); + } + + bool _isLightColor(Color color) { + final luminance = color.computeLuminance(); + return luminance > 0.5; + } +} diff --git a/lib/widgets/custom_dropdown.dart b/lib/widgets/custom_dropdown.dart new file mode 100644 index 0000000..d84a684 --- /dev/null +++ b/lib/widgets/custom_dropdown.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +class CustomDropdown extends StatelessWidget { + final String value; + final String label; + final IconData? icon; + final List items; + final void Function(String?) onChanged; + final String? Function(String?)? validator; + + const CustomDropdown({ + super.key, + required this.value, + required this.label, + required this.items, + required this.onChanged, + this.icon, + this.validator, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + SizedBox(height: 8), + DropdownButtonFormField( + initialValue: items.contains(value) ? value : null, + decoration: InputDecoration( + prefixIcon: icon != null + ? Icon(icon, color: Colors.blue.shade400, size: 22) + : null, + filled: true, + fillColor: Colors.white, + contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.blue, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.red.shade300), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.red, width: 2), + ), + ), + items: items.map((String item) { + return DropdownMenuItem( + value: item, + child: Text(item), + ); + }).toList(), + onChanged: onChanged, + validator: validator, + dropdownColor: Colors.white, + icon: Icon(Icons.arrow_drop_down, color: Colors.blue.shade400), + ), + ], + ); + } +} diff --git a/lib/widgets/custom_text_field.dart b/lib/widgets/custom_text_field.dart new file mode 100644 index 0000000..31d841f --- /dev/null +++ b/lib/widgets/custom_text_field.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; + +class CustomTextField extends StatelessWidget { + final TextEditingController controller; + final String label; + final String? hint; + final IconData? icon; + final TextInputType keyboardType; + final String? Function(String?)? validator; + final int maxLines; + final bool readOnly; + final VoidCallback? onTap; + final Widget? suffix; + + const CustomTextField({ + super.key, + required this.controller, + required this.label, + this.hint, + this.icon, + this.keyboardType = TextInputType.text, + this.validator, + this.maxLines = 1, + this.readOnly = false, + this.onTap, + this.suffix, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Colors.grey.shade700, + ), + ), + SizedBox(height: 8), + TextFormField( + controller: controller, + keyboardType: keyboardType, + validator: validator, + maxLines: maxLines, + readOnly: readOnly, + onTap: onTap, + decoration: InputDecoration( + hintText: hint, + prefixIcon: icon != null + ? Icon(icon, color: Colors.blue.shade400, size: 22) + : null, + suffixIcon: suffix, + filled: true, + fillColor: Colors.white, + contentPadding: EdgeInsets.symmetric( + horizontal: 16, + vertical: maxLines > 1 ? 16 : 14, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.blue, width: 2), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.red.shade300), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Colors.red, width: 2), + ), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/section_header.dart b/lib/widgets/section_header.dart new file mode 100644 index 0000000..7d1aac5 --- /dev/null +++ b/lib/widgets/section_header.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +class SectionHeader extends StatelessWidget { + final String title; + final IconData icon; + final Color color; + + const SectionHeader({ + super.key, + required this.title, + required this.icon, + this.color = Colors.blue, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + Container( + padding: EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withAlpha(26), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, color: color, size: 20), + ), + SizedBox(width: 12), + Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.grey.shade800, + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.only(left: 12), + height: 2, + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + color.withAlpha(77), + Colors.transparent, + ], + ), + ), + ), + ), + ], + ), + ); + } +}