From aae239fb68ea3ab542d2f247e1f78f289fa2c99d Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Mon, 28 Jul 2025 10:51:21 +0200 Subject: [PATCH 01/16] Merge with 13-creating-event --- lib/l10n/app_en.arb | 38 +++- lib/l10n/app_pl.arb | 38 +++- .../core/presentation/app_initializer.dart | 5 +- .../data/data_sources/event_data_source.dart | 5 + .../repositories/remote_event_repository.dart | 5 + .../create_event_usecase_provider.dart | 8 + .../domain/repositories/event_repository.dart | 1 + .../domain/usecases/create_event_usecase.dart | 12 ++ .../controllers/event_controller.dart | 90 ++++++++- .../pages/event_creation_page.dart | 70 +++++++ .../create_event_controller_provider.dart | 28 +++ .../providers/event_controller_provider.dart | 12 +- .../widgets/event_author_tile.dart | 12 +- .../widgets/event_creation_form.dart | 142 ++++++++++++++ .../event_form/age_category_dropdown.dart | 40 ++++ .../event_form/coorinates_map_picker.dart | 84 +++++++++ .../widgets/event_form/date_picker_field.dart | 175 ++++++++++++++++++ .../widgets/event_form/descritpion_field.dart | 28 +++ .../widgets/event_form/image_url_field.dart | 28 +++ .../widgets/event_form/is_paid_checkbox.dart | 34 ++++ .../widgets/event_form/location_section.dart | 47 +++++ .../widgets/event_form/status_dropdown.dart | 40 ++++ .../event_form/submit_button_section.dart | 33 ++++ .../widgets/event_form/title_field.dart | 31 ++++ .../shared/domain/models/age_category.dart | 27 +++ lib/src/shared/domain/models/event_model.dart | 42 +++-- .../shared/domain/models/event_status.dart | 29 +++ .../shared/presentation/theme/app_theme.dart | 15 ++ .../widgets/custom_text_field.dart | 6 + 29 files changed, 1099 insertions(+), 26 deletions(-) create mode 100644 lib/src/features/event/domain/providers/create_event_usecase_provider.dart create mode 100644 lib/src/features/event/domain/usecases/create_event_usecase.dart create mode 100644 lib/src/features/event/presentation/pages/event_creation_page.dart create mode 100644 lib/src/features/event/presentation/providers/create_event_controller_provider.dart create mode 100644 lib/src/features/event/presentation/widgets/event_creation_form.dart create mode 100644 lib/src/features/event/presentation/widgets/event_form/age_category_dropdown.dart create mode 100644 lib/src/features/event/presentation/widgets/event_form/coorinates_map_picker.dart create mode 100644 lib/src/features/event/presentation/widgets/event_form/date_picker_field.dart create mode 100644 lib/src/features/event/presentation/widgets/event_form/descritpion_field.dart create mode 100644 lib/src/features/event/presentation/widgets/event_form/image_url_field.dart create mode 100644 lib/src/features/event/presentation/widgets/event_form/is_paid_checkbox.dart create mode 100644 lib/src/features/event/presentation/widgets/event_form/location_section.dart create mode 100644 lib/src/features/event/presentation/widgets/event_form/status_dropdown.dart create mode 100644 lib/src/features/event/presentation/widgets/event_form/submit_button_section.dart create mode 100644 lib/src/features/event/presentation/widgets/event_form/title_field.dart create mode 100644 lib/src/shared/domain/models/age_category.dart create mode 100644 lib/src/shared/domain/models/event_status.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 440d84d..8872e1f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -72,5 +72,41 @@ "navbarProfile": "Profile", "seeDetails": "See details", "information": "Information", - "noInformation": "No information available." + "noInformation": "No information available.", + "titleLabel": "Title", + "titleHint": "Enter event title", + "titleRequiredError": "Please enter a title", + "descriptionLabel": "Description", + "descriptionHint": "Add event description", + "startDateLabel": "Start date", + "startDateHint": "Select event start date", + "endDateLabel": "End date", + "endDateHint": "Select event end date", + "endDateError": "End date must be later than start date", + "locationLabel": "Location", + "locationHint": "Enter event location", + "addressLabel": "Address", + "addressHint": "Enter event address", + "latitudeLabel": "Latitude", + "longitudeLabel": "Longitude", + "imageUrlLabel": "Image URL", + "imageUrlHint": "Enter event image URL", + "statusLabel": "Status", + "statusHint": "Select event status", + "ageCategoryLabel": "Age category", + "ageCategoryHint": "Select event age category", + "createEventButton": "Create event", + "ageEveryone": "Everyone", + "ageAdults": "Adults", + "ageTeens": "Teens", + "ageKids": "Kids", + "statusDraft": "Draft", + "statusPublished": "Published", + "statusOngoing": "Ongoing", + "statusEnded": "Ended", + "statusCanceled": "Canceled", + "eventCreatedSuccess": "Event created successfully", + "isPaidLabel": "Is the event paid?", + "selectPointonMapLabel": "Select location on map", + "locationRequiredError": "Please enter a location" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 1fdbe3c..b203d46 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -72,5 +72,41 @@ "navbarProfile": "Profil", "seeDetails": "Zobacz szczegóły", "information": "Informacje", - "noInformation": "Brak dostępnych informacji." + "noInformation": "Brak dostępnych informacji.", + "titleLabel": "Tytuł", + "titleHint": "Wprowadź tytuł wydarzenia", + "titleRequiredError": "Podaj tytuł", + "descriptionLabel": "Opis", + "descriptionHint": "Dodaj opis wydarzenia", + "startDateLabel": "Data rozpoczęcia", + "startDateHint": "Wybierz datę rozpoczęcia", + "endDateLabel": "Data zakończenia", + "endDateHint": "Wybierz datę zakończenia", + "endDateError": "Data zakończenia musi być późniejsza niż data rozpoczęcia", + "locationLabel": "Lokalizacja", + "locationHint": "Wprowadź lokalizację", + "addressLabel": "Adres", + "addressHint": "Wprowadź adres", + "latitudeLabel": "Szerokość geograficzna", + "longitudeLabel": "Długość geograficzna", + "imageUrlLabel": "URL obrazu", + "imageUrlHint": "Wprowadź URL obrazu", + "statusLabel": "Status", + "statusHint": "Wybierz status", + "ageCategoryLabel": "Kategoria wiekowa", + "ageCategoryHint": "Wybierz kategorię wiekową", + "createEventButton": "Utwórz wydarzenie", + "ageEveryone": "Wszyscy", + "ageAdults": "Dorośli", + "ageTeens": "Nastolatki", + "ageKids": "Dzieci", + "statusDraft": "Szkic", + "statusPublished": "Opublikowane", + "statusOngoing": "Trwające", + "statusEnded": "Zakończone", + "statusCanceled": "Anulowane", + "eventCreatedSuccess": "Wydarzenie zostało utworzone pomyślnie", + "isPaidLabel": "Czy wydarzenie jest płatne?", + "selectPointonMapLabel": "Wybierz lokalizacje na mapie", + "locationRequiredError": "Podaj lokalizację" } diff --git a/lib/src/core/presentation/app_initializer.dart b/lib/src/core/presentation/app_initializer.dart index 595b603..cede5c0 100644 --- a/lib/src/core/presentation/app_initializer.dart +++ b/lib/src/core/presentation/app_initializer.dart @@ -8,6 +8,7 @@ import 'package:interns2025b_mobile/src/core/routes/app_routes.dart'; import 'package:interns2025b_mobile/src/features/auth/presentation/pages/forgot_password_page.dart'; import 'package:interns2025b_mobile/src/features/auth/presentation/pages/login_page.dart'; import 'package:interns2025b_mobile/src/features/auth/presentation/pages/register_page.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/pages/event_creation_page.dart'; import 'package:interns2025b_mobile/src/features/event/presentation/pages/event_details_page.dart'; import 'package:interns2025b_mobile/src/features/event/presentation/pages/event_page.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/pages/profile_page.dart'; @@ -40,10 +41,10 @@ class AppInitializer extends ConsumerWidget { AppRoutes.login: (context) => const LoginPage(), AppRoutes.forgotPassword: (context) => const ForgotPasswordPage(), AppRoutes.profile: (context) => const ProfilePage(), - AppRoutes.addEvent: (context) => Placeholder(), + AppRoutes.addEvent: (context) => EventCreationPage(), AppRoutes.events: (context) => EventPage(), AppRoutes.eventDetails: (context) => EventDetails(), }, ); } -} +} \ No newline at end of file diff --git a/lib/src/features/event/data/data_sources/event_data_source.dart b/lib/src/features/event/data/data_sources/event_data_source.dart index eba63e4..1a39e6e 100644 --- a/lib/src/features/event/data/data_sources/event_data_source.dart +++ b/lib/src/features/event/data/data_sources/event_data_source.dart @@ -18,4 +18,9 @@ class EventDataSource { final response = await httpClient.get('/api/events/$id'); return Event.fromJson(response['data'] as Map); } + + Future createEvent(Event event) async { + final body = event.toJson(); + await httpClient.post('/api/events', body: body); + } } diff --git a/lib/src/features/event/data/repositories/remote_event_repository.dart b/lib/src/features/event/data/repositories/remote_event_repository.dart index 29ec597..027ccf4 100644 --- a/lib/src/features/event/data/repositories/remote_event_repository.dart +++ b/lib/src/features/event/data/repositories/remote_event_repository.dart @@ -16,4 +16,9 @@ class RemoteEventRepository implements EventRepository { Future getEventById(int id) { return dataSource.getEventById(id); } + + @override + Future createEvent(Event event) { + return dataSource.createEvent(event); + } } diff --git a/lib/src/features/event/domain/providers/create_event_usecase_provider.dart b/lib/src/features/event/domain/providers/create_event_usecase_provider.dart new file mode 100644 index 0000000..0bbd1d1 --- /dev/null +++ b/lib/src/features/event/domain/providers/create_event_usecase_provider.dart @@ -0,0 +1,8 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/src/features/event/domain/usecases/create_event_usecase.dart'; +import 'package:interns2025b_mobile/src/features/event/data/providers/event_repository_provider.dart'; + +final createEventUseCaseProvider = Provider((ref) { + final repository = ref.watch(eventRepositoryProvider); + return CreateEventUseCase(repository); +}); diff --git a/lib/src/features/event/domain/repositories/event_repository.dart b/lib/src/features/event/domain/repositories/event_repository.dart index 08476fe..3fdd592 100644 --- a/lib/src/features/event/domain/repositories/event_repository.dart +++ b/lib/src/features/event/domain/repositories/event_repository.dart @@ -3,4 +3,5 @@ import 'package:interns2025b_mobile/src/shared/domain/models/event_model.dart'; abstract class EventRepository { Future> getEvents({int page = 1}); Future getEventById(int id); + Future createEvent(Event event); } diff --git a/lib/src/features/event/domain/usecases/create_event_usecase.dart b/lib/src/features/event/domain/usecases/create_event_usecase.dart new file mode 100644 index 0000000..fa82b10 --- /dev/null +++ b/lib/src/features/event/domain/usecases/create_event_usecase.dart @@ -0,0 +1,12 @@ +import 'package:interns2025b_mobile/src/shared/domain/models/event_model.dart'; +import 'package:interns2025b_mobile/src/features/event/domain/repositories/event_repository.dart'; + +class CreateEventUseCase { + final EventRepository repository; + + CreateEventUseCase(this.repository); + + Future call(Event event) { + return repository.createEvent(event); + } +} diff --git a/lib/src/features/event/presentation/controllers/event_controller.dart b/lib/src/features/event/presentation/controllers/event_controller.dart index 6dc4398..c898489 100644 --- a/lib/src/features/event/presentation/controllers/event_controller.dart +++ b/lib/src/features/event/presentation/controllers/event_controller.dart @@ -2,13 +2,21 @@ import 'package:flutter/material.dart'; import 'package:interns2025b_mobile/src/core/exceptions/auth_exception.dart'; import 'package:interns2025b_mobile/src/core/exceptions/http_exception.dart'; import 'package:interns2025b_mobile/src/core/exceptions/no_internet_exception.dart'; +import 'package:interns2025b_mobile/src/features/event/domain/usecases/create_event_usecase.dart'; import 'package:interns2025b_mobile/src/features/event/domain/usecases/get_events_usecase.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/age_category.dart'; import 'package:interns2025b_mobile/src/shared/domain/models/event_model.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/event_status.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/owner_type.dart'; class EventsController extends ChangeNotifier { final GetEventsUseCase getEventsUseCase; + final CreateEventUseCase createEventUseCase; - EventsController({required this.getEventsUseCase}); + EventsController({ + required this.getEventsUseCase, + required this.createEventUseCase, + }); final List _events = []; final Set _shownEventIds = {}; @@ -25,12 +33,71 @@ class EventsController extends ChangeNotifier { String? _errorMessage; String? get errorMessage => _errorMessage; + bool _isCreating = false; + bool get isCreating => _isCreating; + + String? _creationError; + String? get creationError => _creationError; + bool _hasMore = true; bool get hasMore => _hasMore; String _searchQuery = ''; String get searchQuery => _searchQuery; + + EventStatus? selectedStatus; + AgeCategory? selectedAgeCategory; + DateTime? startDate; + DateTime? endDate; + bool isPaid = false; + + + void updateFormData({ + DateTime? start, + DateTime? end, + bool? paid, + EventStatus? status, + AgeCategory? ageCategory, + }) { + if (start != null) startDate = start; + if (end != null) endDate = end; + if (paid != null) isPaid = paid; + if (status != null) selectedStatus = status; + if (ageCategory != null) selectedAgeCategory = ageCategory; + + notifyListeners(); + } + + Event buildEvent({ + required String title, + required String description, + required String location, + required String? address, + required String? latitude, + required String? longitude, + required String imageUrl, + }) { + return Event( + id: 0, + title: title, + description: description, + start: startDate, + end: endDate, + location: location, + address: address, + latitude: double.tryParse(latitude ?? ''), + longitude: double.tryParse(longitude ?? ''), + isPaid: isPaid, + price: null, + status: selectedStatus ?? EventStatus.draft, + imageUrl: imageUrl, + ageCategory: selectedAgeCategory?.name, + ownerType: OwnerType.user, + ownerId: 1, + ); + } + List get events => _events; List get filteredEvents { @@ -140,4 +207,25 @@ class EventsController extends ChangeNotifier { notifyListeners(); } } + + Future createEvent(Event event) async { + _isCreating = true; + _creationError = null; + notifyListeners(); + + try { + await createEventUseCase(event); + } on NoInternetException catch (e) { + _creationError = e.message; + } on HttpException catch (e) { + _creationError = 'Server error: ${e.message} (code ${e.statusCode})'; + } on AuthException catch (e) { + _creationError = 'Authorization error: ${e.message}'; + } catch (e) { + _creationError = 'Unexpected error: $e'; + } finally { + _isCreating = false; + notifyListeners(); + } + } } diff --git a/lib/src/features/event/presentation/pages/event_creation_page.dart b/lib/src/features/event/presentation/pages/event_creation_page.dart new file mode 100644 index 0000000..27d47e4 --- /dev/null +++ b/lib/src/features/event/presentation/pages/event_creation_page.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/navigation_bar.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_creation_form.dart'; + +class EventCreationPage extends ConsumerStatefulWidget { + const EventCreationPage({super.key}); + + @override + ConsumerState createState() => _EventCreationPageState(); +} + +class _EventCreationPageState extends ConsumerState { + final _formKey = GlobalKey(); + + final title = TextEditingController(); + final description = TextEditingController(); + final startTime = TextEditingController(); + final endTime = TextEditingController(); + final location = TextEditingController(); + final address = TextEditingController(); + final latitude = TextEditingController(); + final longitude = TextEditingController(); + final imageUrl = TextEditingController(); + final status = TextEditingController(); + final ageCategory = TextEditingController(); + + @override + void dispose() { + title.dispose(); + description.dispose(); + startTime.dispose(); + endTime.dispose(); + location.dispose(); + address.dispose(); + latitude.dispose(); + longitude.dispose(); + imageUrl.dispose(); + status.dispose(); + ageCategory.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Create Event'), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: EventCreationForm( + formKey: _formKey, + title: title, + description: description, + startTime: startTime, + endTime: endTime, + location: location, + address: address, + latitude: latitude, + longitude: longitude, + imageUrl: imageUrl, + status: status, + ageCategory: ageCategory, + ), + ), + bottomNavigationBar: const NavigationBarWidget(), + ); + } +} diff --git a/lib/src/features/event/presentation/providers/create_event_controller_provider.dart b/lib/src/features/event/presentation/providers/create_event_controller_provider.dart new file mode 100644 index 0000000..7451749 --- /dev/null +++ b/lib/src/features/event/presentation/providers/create_event_controller_provider.dart @@ -0,0 +1,28 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/src/features/event/domain/providers/create_event_usecase_provider.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/event_model.dart'; +import 'package:interns2025b_mobile/src/features/event/domain/usecases/create_event_usecase.dart'; + +final createEventControllerProvider = +StateNotifierProvider>((ref) { + final useCase = ref.watch(createEventUseCaseProvider); + return CreateEventController(useCase); +}); + +class CreateEventController extends StateNotifier> { + final CreateEventUseCase createEventUseCase; + + CreateEventController(this.createEventUseCase) + : super(const AsyncValue.data(null)); + + Future create(Event event) async { + state = const AsyncValue.loading(); + + try { + await createEventUseCase(event); + state = const AsyncValue.data(null); + } catch (e, st) { + state = AsyncValue.error(e, st); + } + } +} diff --git a/lib/src/features/event/presentation/providers/event_controller_provider.dart b/lib/src/features/event/presentation/providers/event_controller_provider.dart index b46a014..b073ca3 100644 --- a/lib/src/features/event/presentation/providers/event_controller_provider.dart +++ b/lib/src/features/event/presentation/providers/event_controller_provider.dart @@ -1,10 +1,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/src/features/event/domain/providers/create_event_usecase_provider.dart'; import 'package:interns2025b_mobile/src/features/event/domain/providers/get_events_usecase_provider.dart'; import 'package:interns2025b_mobile/src/features/event/presentation/controllers/event_controller.dart'; -final eventsControllerProvider = ChangeNotifierProvider(( - ref, -) { +final eventsControllerProvider = ChangeNotifierProvider((ref) { final getEventsUseCase = ref.watch(getEventsUseCaseProvider); - return EventsController(getEventsUseCase: getEventsUseCase)..loadEvents(); + final createEventUseCase = ref.watch(createEventUseCaseProvider); + + return EventsController( + getEventsUseCase: getEventsUseCase, + createEventUseCase: createEventUseCase, + )..loadEvents(); }); diff --git a/lib/src/features/event/presentation/widgets/event_author_tile.dart b/lib/src/features/event/presentation/widgets/event_author_tile.dart index a412d1a..5cf9dc3 100644 --- a/lib/src/features/event/presentation/widgets/event_author_tile.dart +++ b/lib/src/features/event/presentation/widgets/event_author_tile.dart @@ -22,12 +22,12 @@ class EventAuthorTile extends StatelessWidget { color: AppColors.primary.withValues(alpha: 0.1), child: owner.avatarUrl != null && owner.avatarUrl!.isNotEmpty ? Image.network( - owner.avatarUrl!, - fit: BoxFit.cover, - errorBuilder: (context, error, stackTrace) { - return _defaultAvatar(); - }, - ) + owner.avatarUrl!, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) { + return _defaultAvatar(); + }, + ) : _defaultAvatar(), ), ), diff --git a/lib/src/features/event/presentation/widgets/event_creation_form.dart b/lib/src/features/event/presentation/widgets/event_creation_form.dart new file mode 100644 index 0000000..3b0deab --- /dev/null +++ b/lib/src/features/event/presentation/widgets/event_creation_form.dart @@ -0,0 +1,142 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/core/routes/app_routes.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/providers/event_controller_provider.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_form/age_category_dropdown.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_form/coorinates_map_picker.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_form/date_picker_field.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_form/descritpion_field.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_form/image_url_field.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_form/is_paid_checkbox.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_form/location_section.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_form/status_dropdown.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_form/submit_button_section.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_form/title_field.dart'; + +class EventCreationForm extends ConsumerStatefulWidget { + final GlobalKey formKey; + final TextEditingController title; + final TextEditingController description; + final TextEditingController startTime; + final TextEditingController endTime; + final TextEditingController location; + final TextEditingController? address; + final TextEditingController? latitude; + final TextEditingController? longitude; + final TextEditingController imageUrl; + final TextEditingController status; + final TextEditingController ageCategory; + + const EventCreationForm({ + super.key, + required this.formKey, + required this.title, + required this.description, + required this.startTime, + required this.endTime, + required this.location, + required this.address, + required this.latitude, + required this.longitude, + required this.imageUrl, + required this.status, + required this.ageCategory, + }); + + @override + ConsumerState createState() => _EventCreationFormState(); +} + +class _EventCreationFormState extends ConsumerState { + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + final controller = ref.watch(eventsControllerProvider); + + return Form( + key: widget.formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TitleField(controller: widget.title), + DescriptionField(controller: widget.description), + DatePickerField( + startController: widget.startTime, + endController: widget.endTime, + onStartPicked: (date) => controller.updateFormData(start: date), + onEndPicked: (date) => controller.updateFormData(end: date), + startDate: controller.startDate, + endDate: controller.endDate, + ), + LocationSection(location: widget.location, address: widget.address!), + CoordinatesMapPicker( + latitude: widget.latitude!, + longitude: widget.longitude!, + ), + IsPaidCheckbox( + value: controller.isPaid, + onChanged: (bool? val) => controller.updateFormData(paid: val ?? false), + ), + ImageUrlField(controller: widget.imageUrl), + StatusDropdown( + selected: controller.selectedStatus, + onChanged: (status) { + if (status != null) { + controller.updateFormData(status: status); + widget.status.text = status.name; + } + } + ), + AgeCategoryDropdown( + selected: controller.selectedAgeCategory, + onChanged: (category) { + controller.updateFormData(ageCategory: category); + widget.ageCategory.text = category?.name ?? ''; + } + ), + SubmitButtonSection( + isLoading: controller.isCreating, + onPressed: () async { + if (!widget.formKey.currentState!.validate()) { + return; + } + + final ScaffoldMessengerState scaffoldMessenger = + ScaffoldMessenger.of(context); + final NavigatorState navigator = Navigator.of(context); + final String successMsg = localizations.eventCreatedSuccess; + + final event = controller.buildEvent( + title: widget.title.text, + description: widget.description.text, + location: widget.location.text, + address: widget.address?.text, + latitude: widget.latitude?.text, + longitude: widget.longitude?.text, + imageUrl: widget.imageUrl.text, + ); + + await controller.createEvent(event); + + if (!mounted) { + return; + } + + if (controller.creationError == null) { + scaffoldMessenger.showSnackBar( + SnackBar(content: Text(successMsg)), + ); + navigator.pushReplacementNamed(AppRoutes.events); + } else { + scaffoldMessenger.showSnackBar( + SnackBar(content: Text(controller.creationError!)), + ); + } + }, + ), + ], + ), + ); + } +} diff --git a/lib/src/features/event/presentation/widgets/event_form/age_category_dropdown.dart b/lib/src/features/event/presentation/widgets/event_form/age_category_dropdown.dart new file mode 100644 index 0000000..e11cb70 --- /dev/null +++ b/lib/src/features/event/presentation/widgets/event_form/age_category_dropdown.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/age_category.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/labeled_text.dart'; + +class AgeCategoryDropdown extends StatelessWidget { + final AgeCategory? selected; + final ValueChanged onChanged; + + const AgeCategoryDropdown({ + super.key, + required this.selected, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LabeledText(localizations.ageCategoryLabel), + const SizedBox(height: 8), + DropdownButtonFormField( + value: selected, + items: AgeCategory.values.map((category) { + return DropdownMenuItem( + value: category, + child: Text(category.label(context)), + ); + }).toList(), + onChanged: onChanged, + validator: (value) => + value == null ? localizations.ageCategoryHint : null, + ), + ], + ); + } +} diff --git a/lib/src/features/event/presentation/widgets/event_form/coorinates_map_picker.dart b/lib/src/features/event/presentation/widgets/event_form/coorinates_map_picker.dart new file mode 100644 index 0000000..7f711e0 --- /dev/null +++ b/lib/src/features/event/presentation/widgets/event_form/coorinates_map_picker.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/pin_map_title_layer.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/theme/app_colors.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/labeled_text.dart'; +import 'package:latlong2/latlong.dart'; + +class CoordinatesMapPicker extends StatefulWidget { + final TextEditingController latitude; + final TextEditingController longitude; + + const CoordinatesMapPicker({ + super.key, + required this.latitude, + required this.longitude, + }); + + @override + State createState() => _CoordinatesMapPickerState(); +} + +class _CoordinatesMapPickerState extends State { + late LatLng _selectedPosition; + final MapController _mapController = MapController(); + + @override + void initState() { + super.initState(); + final lat = double.tryParse(widget.latitude.text) ?? 51.2081; + final lng = double.tryParse(widget.longitude.text) ?? 16.1603; + _selectedPosition = LatLng(lat, lng); + } + + void _updatePosition(LatLng newPos) { + setState(() => _selectedPosition = newPos); + widget.latitude.text = newPos.latitude.toStringAsFixed(6); + widget.longitude.text = newPos.longitude.toStringAsFixed(6); + } + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LabeledText(localizations.selectPointonMapLabel), + const SizedBox(height: 8), + SizedBox( + height: 250, + child: FlutterMap( + mapController: _mapController, + options: MapOptions( + initialCenter: _selectedPosition, + initialZoom: 15, + minZoom: 10, + maxZoom: 24, + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds(LatLng(51.14, 16.06), LatLng(51.28, 16.26)), + ), + onTap: (tapPos, point) => _updatePosition(point), + interactionOptions: const InteractionOptions( + flags: InteractiveFlag.all & ~InteractiveFlag.rotate, + ), + ), + children: [ + PinMapTitleLayer(), + MarkerLayer( + markers: [ + Marker( + point: _selectedPosition, + width: 40, + height: 40, + child: const Icon(Icons.location_on, size: 40, color: AppColors.primary), + ), + ], + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/src/features/event/presentation/widgets/event_form/date_picker_field.dart b/lib/src/features/event/presentation/widgets/event_form/date_picker_field.dart new file mode 100644 index 0000000..444960d --- /dev/null +++ b/lib/src/features/event/presentation/widgets/event_form/date_picker_field.dart @@ -0,0 +1,175 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/custom_text_field.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/labeled_text.dart'; + +class DatePickerField extends StatefulWidget { + final TextEditingController startController; + final TextEditingController endController; + final DateTime? startDate; + final DateTime? endDate; + final ValueChanged onStartPicked; + final ValueChanged onEndPicked; + + const DatePickerField({ + super.key, + required this.startController, + required this.endController, + required this.startDate, + required this.endDate, + required this.onStartPicked, + required this.onEndPicked, + }); + + @override + State createState() => _DatePickerFieldState(); +} + +class _DatePickerFieldState extends State { + DateTime? _startDate; + DateTime? _endDate; + + @override + void initState() { + super.initState(); + _startDate = widget.startDate; + _endDate = widget.endDate; + } + + Future _pickDateTime({ + required DateTime initialDate, + required DateTime firstDate, + required void Function(DateTime) onPicked, + required TextEditingController controller, + required bool isStart, + }) async { + final pickedDate = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: firstDate, + lastDate: DateTime(2100), + builder: (BuildContext dialogContext, Widget? child) { + return Theme(data: Theme.of(dialogContext), child: child!); + }, + ); + + if (!mounted || pickedDate == null) return; + + final pickedTime = await showDialog( + context: context, + builder: (BuildContext dialogContext) { + final mediaQuery = MediaQuery.of( + dialogContext, + ).copyWith(alwaysUse24HourFormat: true); + + return MediaQuery( + data: mediaQuery, + child: Theme( + data: Theme.of(dialogContext), + child: TimePickerDialog( + initialTime: TimeOfDay.fromDateTime(initialDate), + initialEntryMode: TimePickerEntryMode.inputOnly, + ), + ), + ); + }, + ); + + if (!mounted || pickedTime == null) return; + + final fullDateTime = DateTime( + pickedDate.year, + pickedDate.month, + pickedDate.day, + pickedTime.hour, + pickedTime.minute, + ); + + controller.text = _formatDateTime(fullDateTime); + onPicked(fullDateTime); + + setState(() { + if (isStart) { + _startDate = fullDateTime; + if (_endDate != null && _endDate!.isBefore(_startDate!)) { + _endDate = null; + widget.endController.clear(); + } + } else { + _endDate = fullDateTime; + } + }); + } + + String _formatDateTime(DateTime dateTime) { + final date = + "${dateTime.year.toString().padLeft(4, '0')}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}"; + final time = + "${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}"; + return "$date $time"; + } + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LabeledText(localizations.startDateLabel), + const SizedBox(height: 8), + GestureDetector( + onTap: () => _pickDateTime( + initialDate: _startDate ?? DateTime.now(), + firstDate: DateTime.now(), + onPicked: widget.onStartPicked, + controller: widget.startController, + isStart: true, + ), + child: AbsorbPointer( + child: CustomTextField( + controller: widget.startController, + hintText: localizations.startDateHint, + readOnly: true, + validator: (_) { + if (_startDate == null) { + return localizations.startDateHint; + } + return null; + }, + ), + ), + ), + const SizedBox(height: 20), + LabeledText(localizations.endDateLabel), + const SizedBox(height: 8), + GestureDetector( + onTap: () => _pickDateTime( + initialDate: _endDate ?? _startDate ?? DateTime.now(), + firstDate: _startDate ?? DateTime.now(), + onPicked: widget.onEndPicked, + controller: widget.endController, + isStart: false, + ), + child: AbsorbPointer( + child: CustomTextField( + controller: widget.endController, + hintText: localizations.endDateHint, + readOnly: true, + validator: (_) { + if (_endDate == null) { + return localizations.endDateHint; + } + if (_startDate != null && _endDate!.isBefore(_startDate!)) { + return localizations.endDateError; + } + return null; + }, + ), + ), + ), + const SizedBox(height: 20), + ], + ); + } +} diff --git a/lib/src/features/event/presentation/widgets/event_form/descritpion_field.dart b/lib/src/features/event/presentation/widgets/event_form/descritpion_field.dart new file mode 100644 index 0000000..8f5b304 --- /dev/null +++ b/lib/src/features/event/presentation/widgets/event_form/descritpion_field.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/custom_text_field.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/labeled_text.dart'; + +class DescriptionField extends StatelessWidget { + final TextEditingController controller; + + const DescriptionField({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LabeledText(localizations.descriptionLabel), + const SizedBox(height: 8), + CustomTextField( + controller: controller, + hintText: localizations.descriptionHint, + ), + const SizedBox(height: 20), + ], + ); + } +} diff --git a/lib/src/features/event/presentation/widgets/event_form/image_url_field.dart b/lib/src/features/event/presentation/widgets/event_form/image_url_field.dart new file mode 100644 index 0000000..b53c984 --- /dev/null +++ b/lib/src/features/event/presentation/widgets/event_form/image_url_field.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/custom_text_field.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/labeled_text.dart'; + +class ImageUrlField extends StatelessWidget { + final TextEditingController controller; + + const ImageUrlField({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LabeledText(localizations.imageUrlLabel), + const SizedBox(height: 8), + CustomTextField( + controller: controller, + hintText: localizations.imageUrlHint, + ), + const SizedBox(height: 20), + ], + ); + } +} diff --git a/lib/src/features/event/presentation/widgets/event_form/is_paid_checkbox.dart b/lib/src/features/event/presentation/widgets/event_form/is_paid_checkbox.dart new file mode 100644 index 0000000..2eb50d7 --- /dev/null +++ b/lib/src/features/event/presentation/widgets/event_form/is_paid_checkbox.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/labeled_text.dart'; + +class IsPaidCheckbox extends StatelessWidget { + final bool value; + final ValueChanged onChanged; + + const IsPaidCheckbox({ + super.key, + required this.value, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 20), + LabeledText(localizations.isPaidLabel), + CheckboxListTile( + value: value, + onChanged: onChanged, + controlAffinity: ListTileControlAffinity.leading, + contentPadding: EdgeInsets.zero, + ), + const SizedBox(height: 8), + ], + ); + } +} diff --git a/lib/src/features/event/presentation/widgets/event_form/location_section.dart b/lib/src/features/event/presentation/widgets/event_form/location_section.dart new file mode 100644 index 0000000..46635ac --- /dev/null +++ b/lib/src/features/event/presentation/widgets/event_form/location_section.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/custom_text_field.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/labeled_text.dart'; + +class LocationSection extends StatelessWidget { + final TextEditingController location; + final TextEditingController address; + final bool showError; + + const LocationSection({ + super.key, + required this.location, + required this.address, + this.showError = false, + }); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LabeledText(localizations.locationLabel), + const SizedBox(height: 8), + CustomTextField( + controller: location, + hintText: localizations.locationHint, + validator: (value) => value == null || value.isEmpty + ? localizations.locationRequiredError + : null, + ), + const SizedBox(height: 20), + + LabeledText(localizations.addressLabel), + const SizedBox(height: 8), + CustomTextField( + controller: address, + hintText: localizations.addressHint, + + ), + const SizedBox(height: 20), + ], + ); + } +} diff --git a/lib/src/features/event/presentation/widgets/event_form/status_dropdown.dart b/lib/src/features/event/presentation/widgets/event_form/status_dropdown.dart new file mode 100644 index 0000000..6d79eee --- /dev/null +++ b/lib/src/features/event/presentation/widgets/event_form/status_dropdown.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/event_status.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/labeled_text.dart'; + +class StatusDropdown extends StatelessWidget { + final EventStatus? selected; + final ValueChanged onChanged; + + const StatusDropdown({ + super.key, + required this.selected, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LabeledText(localizations.statusLabel), + const SizedBox(height: 8), + DropdownButtonFormField( + value: selected, + items: [EventStatus.draft, EventStatus.published].map((status) { + return DropdownMenuItem( + value: status, + child: Text(status.label(context)), + ); + }).toList(), + onChanged: onChanged, + validator: (value) => value == null ? localizations.statusHint : null, + ), + const SizedBox(height: 20), + ], + ); + } +} diff --git a/lib/src/features/event/presentation/widgets/event_form/submit_button_section.dart b/lib/src/features/event/presentation/widgets/event_form/submit_button_section.dart new file mode 100644 index 0000000..ae45adc --- /dev/null +++ b/lib/src/features/event/presentation/widgets/event_form/submit_button_section.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/button.dart'; + +class SubmitButtonSection extends StatelessWidget { + final VoidCallback onPressed; + final bool isLoading; + + const SubmitButtonSection({ + super.key, + required this.onPressed, + this.isLoading = false, + }); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 20), + Button( + label: localizations.createEventButton, + fullWidth: true, + onPressed: onPressed, + isLoading: isLoading, + ), + const SizedBox(height: 24), + ], + ); + } +} diff --git a/lib/src/features/event/presentation/widgets/event_form/title_field.dart b/lib/src/features/event/presentation/widgets/event_form/title_field.dart new file mode 100644 index 0000000..61870e9 --- /dev/null +++ b/lib/src/features/event/presentation/widgets/event_form/title_field.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/custom_text_field.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/widgets/labeled_text.dart'; + +class TitleField extends StatelessWidget { + final TextEditingController controller; + + const TitleField({super.key, required this.controller}); + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + LabeledText(localizations.titleLabel), + const SizedBox(height: 8), + CustomTextField( + controller: controller, + hintText: localizations.titleHint, + validator: (value) => value == null || value.isEmpty + ? localizations.titleRequiredError + : null, + ), + const SizedBox(height: 20), + ], + ); + } +} diff --git a/lib/src/shared/domain/models/age_category.dart b/lib/src/shared/domain/models/age_category.dart new file mode 100644 index 0000000..72fbcf4 --- /dev/null +++ b/lib/src/shared/domain/models/age_category.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; + +enum AgeCategory { everyone, adults, youth, children } + +extension AgeCategoryX on AgeCategory { + String label(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + switch (this) { + case AgeCategory.everyone: + return localizations.ageEveryone; + case AgeCategory.adults: + return localizations.ageAdults; + case AgeCategory.youth: + return localizations.ageTeens; + case AgeCategory.children: + return localizations.ageKids; + } + } + + static AgeCategory? fromLabel(BuildContext context, String label) { + return AgeCategory.values.firstWhere( + (e) => e.label(context) == label, + orElse: () => AgeCategory.everyone, + ); + } +} diff --git a/lib/src/shared/domain/models/event_model.dart b/lib/src/shared/domain/models/event_model.dart index 30f95bc..934bc3c 100644 --- a/lib/src/shared/domain/models/event_model.dart +++ b/lib/src/shared/domain/models/event_model.dart @@ -1,19 +1,9 @@ +import 'package:interns2025b_mobile/src/shared/domain/models/event_status.dart'; import 'package:interns2025b_mobile/src/shared/domain/models/event_owner.dart'; import 'package:interns2025b_mobile/src/shared/domain/models/organization_model.dart'; import 'package:interns2025b_mobile/src/shared/domain/models/owner_type.dart'; import 'package:interns2025b_mobile/src/shared/domain/models/user_model.dart'; -enum EventStatus { draft, published, ongoing, ended, canceled } - -extension EventStatusX on EventStatus { - static EventStatus fromString(String? value) { - return EventStatus.values.firstWhere( - (e) => e.name == value, - orElse: () => throw ArgumentError('Unknown event status: $value'), - ); - } -} - class Event { final int id; final String title; @@ -104,4 +94,34 @@ class Event { : null, ); } + + Map toJson() { + return { + 'title': title, + 'description': description, + 'start': _formatDate(start), + 'end': _formatDate(end), + 'location': location, + 'address': address, + 'latitude': latitude, + 'longitude': longitude, + 'is_paid': isPaid, + 'price': price, + 'status': status.name, + 'image_url': imageUrl, + 'age_category': ageCategory, + 'owner_type': ownerType.name, + 'owner_id': ownerId, + }; + } + + String? _formatDate(DateTime? dateTime) { + if (dateTime == null) return null; + return '${dateTime.year.toString().padLeft(4, '0')}-' + '${dateTime.month.toString().padLeft(2, '0')}-' + '${dateTime.day.toString().padLeft(2, '0')} ' + '${dateTime.hour.toString().padLeft(2, '0')}:' + '${dateTime.minute.toString().padLeft(2, '0')}:' + '${dateTime.second.toString().padLeft(2, '0')}'; + } } diff --git a/lib/src/shared/domain/models/event_status.dart b/lib/src/shared/domain/models/event_status.dart new file mode 100644 index 0000000..45aba6c --- /dev/null +++ b/lib/src/shared/domain/models/event_status.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; + +enum EventStatus { draft, published, ongoing, ended, canceled } + +extension EventStatusX on EventStatus { + String label(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + switch (this) { + case EventStatus.draft: + return localizations.statusDraft; + case EventStatus.published: + return localizations.statusPublished; + case EventStatus.ongoing: + return localizations.statusOngoing; + case EventStatus.ended: + return localizations.statusEnded; + case EventStatus.canceled: + return localizations.statusCanceled; + } + } + + static EventStatus fromString(String? value) { + return EventStatus.values.firstWhere( + (e) => e.name == value, + orElse: () => throw ArgumentError('Unknown event status: $value'), + ); + } +} diff --git a/lib/src/shared/presentation/theme/app_theme.dart b/lib/src/shared/presentation/theme/app_theme.dart index dcde922..92ac637 100644 --- a/lib/src/shared/presentation/theme/app_theme.dart +++ b/lib/src/shared/presentation/theme/app_theme.dart @@ -38,5 +38,20 @@ class AppTheme { borderRadius: BorderRadius.all(Radius.circular(6)), ), ), + + timePickerTheme: TimePickerThemeData( + hourMinuteColor: WidgetStateColor.resolveWith((Set states) { + if (states.contains(WidgetState.selected)) { + return Colors.green.withValues(alpha: 0.3); + } + return Colors.transparent; + }), + hourMinuteTextColor: WidgetStateColor.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return Colors.green.shade900; + } + return Colors.grey.shade600; + }), + ), ); } diff --git a/lib/src/shared/presentation/widgets/custom_text_field.dart b/lib/src/shared/presentation/widgets/custom_text_field.dart index ffa5be6..67c47ee 100644 --- a/lib/src/shared/presentation/widgets/custom_text_field.dart +++ b/lib/src/shared/presentation/widgets/custom_text_field.dart @@ -8,6 +8,8 @@ class CustomTextField extends StatelessWidget { final Widget? suffixIcon; final String? Function(String?)? validator; final TextInputType keyboardType; + final bool readOnly; + final VoidCallback? onTap; const CustomTextField({ super.key, @@ -17,6 +19,8 @@ class CustomTextField extends StatelessWidget { this.suffixIcon, this.validator, this.keyboardType = TextInputType.text, + this.readOnly = false, + this.onTap, }); @override @@ -25,6 +29,8 @@ class CustomTextField extends StatelessWidget { controller: controller, obscureText: obscureText, keyboardType: keyboardType, + onTap: onTap, + readOnly: readOnly, style: const TextStyle(color: AppColors.text), decoration: InputDecoration( hintText: hintText, From bb05d6cbcdf2058c4746eebdc67bf8d0233b5520 Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Mon, 28 Jul 2025 13:44:58 +0200 Subject: [PATCH 02/16] feat: Display user statistics on profile page --- .../widgets/profile_info_content.dart | 20 +++++++++-- .../presentation/widgets/stat_tile.dart | 35 +++++++++++++++++++ lib/src/shared/domain/models/user_model.dart | 9 +++++ 3 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 lib/src/features/profile/presentation/widgets/stat_tile.dart diff --git a/lib/src/features/profile/presentation/widgets/profile_info_content.dart b/lib/src/features/profile/presentation/widgets/profile_info_content.dart index 19efdcf..5790a72 100644 --- a/lib/src/features/profile/presentation/widgets/profile_info_content.dart +++ b/lib/src/features/profile/presentation/widgets/profile_info_content.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_controller_provider.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/stat_tile.dart'; class ProfileInfoContent extends ConsumerWidget { const ProfileInfoContent({super.key}); @@ -12,15 +13,28 @@ class ProfileInfoContent extends ConsumerWidget { if (user == null) return const SizedBox(); return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 24), Text( user.lastName?.trim().isNotEmpty == true ? '${user.firstName} ${user.lastName}' : user.firstName, - style: Theme.of( - context, - ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 24), + + Row( + children: [ + StatTile(label: 'Wydarzenia', value: user.eventsCount), + const SizedBox(width: 16), + StatTile(label: 'Obserwujący', value: user.followersCount), + const SizedBox(width: 16), + StatTile(label: 'Obserwowani', value: user.followingCount), + ], ), const SizedBox(height: 24), ], diff --git a/lib/src/features/profile/presentation/widgets/stat_tile.dart b/lib/src/features/profile/presentation/widgets/stat_tile.dart new file mode 100644 index 0000000..4e21886 --- /dev/null +++ b/lib/src/features/profile/presentation/widgets/stat_tile.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; + +class StatTile extends StatelessWidget { + final String label; + final int value; + + const StatTile({ + super.key, + required this.label, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + '$value', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w600), + textAlign: TextAlign.center, + ), + Text( + label, + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), + ], + ); + } +} diff --git a/lib/src/shared/domain/models/user_model.dart b/lib/src/shared/domain/models/user_model.dart index 288641c..b69dbbe 100644 --- a/lib/src/shared/domain/models/user_model.dart +++ b/lib/src/shared/domain/models/user_model.dart @@ -10,6 +10,9 @@ class User implements EventOwner { final DateTime? updatedAt; @override final String? avatarUrl; + final int eventsCount; + final int followersCount; + final int followingCount; User({ required this.id, @@ -20,6 +23,9 @@ class User implements EventOwner { required this.createdAt, required this.updatedAt, this.avatarUrl, + required this.eventsCount, + required this.followersCount, + required this.followingCount, }); factory User.fromJson(Map json) { @@ -38,6 +44,9 @@ class User implements EventOwner { updatedAt: json['updated_at'] != null ? DateTime.tryParse(json['updated_at']) : null, + eventsCount: json['events_count'] ?? 0, + followersCount: json['followers_count'] ?? 0, + followingCount: json['following_count'] ?? 0, ); } From 331dcf6bc0e82334752890b183df188104ad57f0 Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Mon, 28 Jul 2025 14:18:47 +0200 Subject: [PATCH 03/16] Fix typo and center profile info content --- .../profile/presentation/widgets/profile_info_content.dart | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/src/features/profile/presentation/widgets/profile_info_content.dart b/lib/src/features/profile/presentation/widgets/profile_info_content.dart index 5790a72..0fff8eb 100644 --- a/lib/src/features/profile/presentation/widgets/profile_info_content.dart +++ b/lib/src/features/profile/presentation/widgets/profile_info_content.dart @@ -13,7 +13,7 @@ class ProfileInfoContent extends ConsumerWidget { if (user == null) return const SizedBox(); return Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ const SizedBox(height: 24), Text( @@ -28,8 +28,9 @@ class ProfileInfoContent extends ConsumerWidget { const SizedBox(height: 24), Row( + mainAxisAlignment: MainAxisAlignment.center, children: [ - StatTile(label: 'Wydarzenia', value: user.eventsCount), + StatTile(label: 'Wydarzeia', value: user.eventsCount), const SizedBox(width: 16), StatTile(label: 'Obserwujący', value: user.followersCount), const SizedBox(width: 16), From 9e10b9ce5eb1ea839c951bcbba5d3b5f6801d6a7 Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Mon, 28 Jul 2025 14:21:05 +0200 Subject: [PATCH 04/16] Fix: Correct typo in "Wydarzenia" label --- .../profile/presentation/widgets/profile_info_content.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/features/profile/presentation/widgets/profile_info_content.dart b/lib/src/features/profile/presentation/widgets/profile_info_content.dart index 0fff8eb..2b9cc4f 100644 --- a/lib/src/features/profile/presentation/widgets/profile_info_content.dart +++ b/lib/src/features/profile/presentation/widgets/profile_info_content.dart @@ -30,7 +30,7 @@ class ProfileInfoContent extends ConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - StatTile(label: 'Wydarzeia', value: user.eventsCount), + StatTile(label: 'Wydarzenia', value: user.eventsCount), const SizedBox(width: 16), StatTile(label: 'Obserwujący', value: user.followersCount), const SizedBox(width: 16), From 6a21e1b71867cd50550ded5bf1c917aa34ed31aa Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Mon, 28 Jul 2025 14:24:27 +0200 Subject: [PATCH 05/16] feat: Localize profile info statistics --- lib/l10n/app_en.arb | 5 ++++- lib/l10n/app_pl.arb | 5 ++++- .../presentation/widgets/profile_info_content.dart | 8 +++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8872e1f..59b4702 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -108,5 +108,8 @@ "eventCreatedSuccess": "Event created successfully", "isPaidLabel": "Is the event paid?", "selectPointonMapLabel": "Select location on map", - "locationRequiredError": "Please enter a location" + "locationRequiredError": "Please enter a location", + "eventsCount": "Events", + "followersCount": "Followers", + "followingCount": "Following" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index b203d46..608bb4b 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -108,5 +108,8 @@ "eventCreatedSuccess": "Wydarzenie zostało utworzone pomyślnie", "isPaidLabel": "Czy wydarzenie jest płatne?", "selectPointonMapLabel": "Wybierz lokalizacje na mapie", - "locationRequiredError": "Podaj lokalizację" + "locationRequiredError": "Podaj lokalizację", + "eventsCount": "Wydarzenia", + "followersCount": "Obserwujący", + "followingCount": "Obserwowani" } diff --git a/lib/src/features/profile/presentation/widgets/profile_info_content.dart b/lib/src/features/profile/presentation/widgets/profile_info_content.dart index 2b9cc4f..e7df038 100644 --- a/lib/src/features/profile/presentation/widgets/profile_info_content.dart +++ b/lib/src/features/profile/presentation/widgets/profile_info_content.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_controller_provider.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/stat_tile.dart'; @@ -9,6 +10,7 @@ class ProfileInfoContent extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(profileControllerProvider).user; + final localizations = AppLocalizations.of(context)!; if (user == null) return const SizedBox(); @@ -30,11 +32,11 @@ class ProfileInfoContent extends ConsumerWidget { Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - StatTile(label: 'Wydarzenia', value: user.eventsCount), + StatTile(label: localizations.eventsCount, value: user.eventsCount), const SizedBox(width: 16), - StatTile(label: 'Obserwujący', value: user.followersCount), + StatTile(label: localizations.followersCount, value: user.followersCount), const SizedBox(width: 16), - StatTile(label: 'Obserwowani', value: user.followingCount), + StatTile(label: localizations.followingCount, value: user.followingCount), ], ), const SizedBox(height: 24), From 33232c0881072b1ed1c431887c11a11824adb76a Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Tue, 29 Jul 2025 08:16:47 +0200 Subject: [PATCH 06/16] Refactor: Extract ProfileStats widget --- .../widgets/profile_info_content.dart | 17 ++------- .../presentation/widgets/profile_stats.dart | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 lib/src/features/profile/presentation/widgets/profile_stats.dart diff --git a/lib/src/features/profile/presentation/widgets/profile_info_content.dart b/lib/src/features/profile/presentation/widgets/profile_info_content.dart index e7df038..885b556 100644 --- a/lib/src/features/profile/presentation/widgets/profile_info_content.dart +++ b/lib/src/features/profile/presentation/widgets/profile_info_content.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_controller_provider.dart'; -import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/stat_tile.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/profile_stats.dart'; class ProfileInfoContent extends ConsumerWidget { const ProfileInfoContent({super.key}); @@ -10,7 +9,6 @@ class ProfileInfoContent extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final user = ref.watch(profileControllerProvider).user; - final localizations = AppLocalizations.of(context)!; if (user == null) return const SizedBox(); @@ -29,17 +27,8 @@ class ProfileInfoContent extends ConsumerWidget { ), const SizedBox(height: 24), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - StatTile(label: localizations.eventsCount, value: user.eventsCount), - const SizedBox(width: 16), - StatTile(label: localizations.followersCount, value: user.followersCount), - const SizedBox(width: 16), - StatTile(label: localizations.followingCount, value: user.followingCount), - ], - ), - const SizedBox(height: 24), + ProfileStats(), + ], ); } diff --git a/lib/src/features/profile/presentation/widgets/profile_stats.dart b/lib/src/features/profile/presentation/widgets/profile_stats.dart new file mode 100644 index 0000000..5e38da3 --- /dev/null +++ b/lib/src/features/profile/presentation/widgets/profile_stats.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_controller_provider.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/stat_tile.dart'; + +class ProfileStats extends ConsumerWidget { + const ProfileStats({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(profileControllerProvider).user; + final localizations = AppLocalizations.of(context)!; + + if (user == null) return const SizedBox(); + + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StatTile(label: localizations.eventsCount, value: user.eventsCount), + const SizedBox(width: 16), + StatTile( + label: localizations.followersCount, + value: user.followersCount, + ), + const SizedBox(width: 16), + StatTile( + label: localizations.followingCount, + value: user.followingCount, + ), + const SizedBox(height: 24), + ], + ); + } +} From 7bc80b77a66ca9464f27dd6051f90abdcd71ad74 Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Tue, 29 Jul 2025 08:19:02 +0200 Subject: [PATCH 07/16] Fix: Reduce spacing in profile stats --- .../features/profile/presentation/widgets/profile_stats.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/features/profile/presentation/widgets/profile_stats.dart b/lib/src/features/profile/presentation/widgets/profile_stats.dart index 5e38da3..98187d9 100644 --- a/lib/src/features/profile/presentation/widgets/profile_stats.dart +++ b/lib/src/features/profile/presentation/widgets/profile_stats.dart @@ -28,7 +28,7 @@ class ProfileStats extends ConsumerWidget { label: localizations.followingCount, value: user.followingCount, ), - const SizedBox(height: 24), + const SizedBox(height: 16), ], ); } From ff1c1417c323ce568428054e047d58dbf0e319bf Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Tue, 29 Jul 2025 11:21:09 +0200 Subject: [PATCH 08/16] feat: Display user events on profile page --- lib/l10n/app_en.arb | 3 +- lib/l10n/app_pl.arb | 3 +- .../profile/domain/utils/event_sorter.dart | 21 ++++++ .../widgets/event_status_badge.dart | 43 ++++++++++++ .../widgets/profile_event_card.dart | 67 +++++++++++++++++++ .../widgets/profile_events_section.dart | 44 ++++++++++++ .../widgets/profile_info_card.dart | 31 ++++----- .../widgets/profile_info_content.dart | 14 ++-- lib/src/shared/domain/models/user_model.dart | 6 ++ .../shared/presentation/theme/app_colors.dart | 1 + 10 files changed, 207 insertions(+), 26 deletions(-) create mode 100644 lib/src/features/profile/domain/utils/event_sorter.dart create mode 100644 lib/src/features/profile/presentation/widgets/event_status_badge.dart create mode 100644 lib/src/features/profile/presentation/widgets/profile_event_card.dart create mode 100644 lib/src/features/profile/presentation/widgets/profile_events_section.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 59b4702..fe86373 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -111,5 +111,6 @@ "locationRequiredError": "Please enter a location", "eventsCount": "Events", "followersCount": "Followers", - "followingCount": "Following" + "followingCount": "Following", + "yourEvents": "Your events" } diff --git a/lib/l10n/app_pl.arb b/lib/l10n/app_pl.arb index 608bb4b..669d9e3 100644 --- a/lib/l10n/app_pl.arb +++ b/lib/l10n/app_pl.arb @@ -111,5 +111,6 @@ "locationRequiredError": "Podaj lokalizację", "eventsCount": "Wydarzenia", "followersCount": "Obserwujący", - "followingCount": "Obserwowani" + "followingCount": "Obserwowani", + "yourEvents": "Twoje wydarzenia" } diff --git a/lib/src/features/profile/domain/utils/event_sorter.dart b/lib/src/features/profile/domain/utils/event_sorter.dart new file mode 100644 index 0000000..9b03b50 --- /dev/null +++ b/lib/src/features/profile/domain/utils/event_sorter.dart @@ -0,0 +1,21 @@ +import 'package:interns2025b_mobile/src/shared/domain/models/event_model.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/event_status.dart'; + +void sortEvents(List events) { + events.sort((a, b) { + bool aIsEndedOrCanceled = + a.status == EventStatus.canceled || a.status == EventStatus.ended; + bool bIsEndedOrCanceled = + b.status == EventStatus.canceled || b.status == EventStatus.ended; + + if (aIsEndedOrCanceled && !bIsEndedOrCanceled) { + return 1; + } else if (!aIsEndedOrCanceled && bIsEndedOrCanceled) { + return -1; + } + + final aDate = a.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0); + final bDate = b.createdAt ?? DateTime.fromMillisecondsSinceEpoch(0); + return bDate.compareTo(aDate); + }); +} diff --git a/lib/src/features/profile/presentation/widgets/event_status_badge.dart b/lib/src/features/profile/presentation/widgets/event_status_badge.dart new file mode 100644 index 0000000..17b8a45 --- /dev/null +++ b/lib/src/features/profile/presentation/widgets/event_status_badge.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/event_status.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/theme/app_colors.dart'; + +class EventStatusBadge extends StatelessWidget { + final EventStatus status; + + const EventStatusBadge({super.key, required this.status}); + + Color get _backgroundColor { + switch (status) { + case EventStatus.draft: + return AppColors.draftEventBadge.withValues(alpha: 0.5); + case EventStatus.published: + return AppColors.blueLabelBackground.withValues(alpha: 0.2); + case EventStatus.ongoing: + return AppColors.primary.withValues(alpha: 0.2); + case EventStatus.canceled: + case EventStatus.ended: + return AppColors.grey.withValues(alpha: 0.2); + } + } + + @override + Widget build(BuildContext context) { + final color = _backgroundColor; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + status.label(context), + style: TextStyle( + color: AppColors.black, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ); + } +} diff --git a/lib/src/features/profile/presentation/widgets/profile_event_card.dart b/lib/src/features/profile/presentation/widgets/profile_event_card.dart new file mode 100644 index 0000000..ba59d3c --- /dev/null +++ b/lib/src/features/profile/presentation/widgets/profile_event_card.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_data_time_row.dart'; +import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_image.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/event_status_badge.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/event_model.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/theme/app_colors.dart'; + +class ProfileEventCard extends StatelessWidget { + final Event event; + + const ProfileEventCard({super.key, required this.event}); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 200), + child: Card( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + clipBehavior: Clip.antiAlias, + elevation: 2, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: 16 / 9, + child: EventImage( + imageUrl: event.imageUrl, + width: double.infinity, + borderRadius: BorderRadius.zero, + ), + ), + Container( + color: AppColors.backgroundLight, + padding: const EdgeInsets.fromLTRB(12, 10, 12, 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + EventDateTimeRow(date: event.start), + const SizedBox(height: 6), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + event.title, + style: Theme.of(context).textTheme.bodyMedium + ?.copyWith( + fontWeight: FontWeight.bold, + color: AppColors.black, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + EventStatusBadge(status: event.status), + ], + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/features/profile/presentation/widgets/profile_events_section.dart b/lib/src/features/profile/presentation/widgets/profile_events_section.dart new file mode 100644 index 0000000..508a001 --- /dev/null +++ b/lib/src/features/profile/presentation/widgets/profile_events_section.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/profile_event_card.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/event_model.dart'; + +class ProfileEventsSection extends ConsumerWidget { + final List events; + + const ProfileEventsSection({super.key, required this.events}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + if (events.isEmpty) { + return const SizedBox(); + } + + final localizations = AppLocalizations.of(context)!; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Text( + localizations.yourEvents, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 12), + SizedBox( + height: 200, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: events.length, + separatorBuilder: (_, _) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final event = events[index]; + return ProfileEventCard(event: event); + }, + ), + ), + ], + ); + } +} diff --git a/lib/src/features/profile/presentation/widgets/profile_info_card.dart b/lib/src/features/profile/presentation/widgets/profile_info_card.dart index 4e027ae..12d42a5 100644 --- a/lib/src/features/profile/presentation/widgets/profile_info_card.dart +++ b/lib/src/features/profile/presentation/widgets/profile_info_card.dart @@ -11,22 +11,16 @@ class ProfileInfoCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final screenHeight = MediaQuery.of(context).size.height; - final topPadding = MediaQuery.of(context).padding.top; - return Stack( clipBehavior: Clip.none, children: [ Container( width: double.infinity, - constraints: BoxConstraints( - minHeight: screenHeight - topPadding - 170, - ), padding: const EdgeInsets.fromLTRB(24, 60, 24, 24), decoration: BoxDecoration( color: AppColors.backgroundLight, - borderRadius: BorderRadius.vertical(top: Radius.circular(50)), - boxShadow: [ + borderRadius: const BorderRadius.vertical(top: Radius.circular(50)), + boxShadow: const [ BoxShadow( color: Colors.black12, blurRadius: 12, @@ -34,17 +28,16 @@ class ProfileInfoCard extends ConsumerWidget { ), ], ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const ProfileInfoContent(), - const SizedBox(height: 24), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 0), - child: ProfileEditSection(), - ), - ], + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: const [ + ProfileInfoContent(), + SizedBox(height: 24), + ProfileEditSection(), + ], + ), ), ), Positioned( diff --git a/lib/src/features/profile/presentation/widgets/profile_info_content.dart b/lib/src/features/profile/presentation/widgets/profile_info_content.dart index 885b556..5afe5d3 100644 --- a/lib/src/features/profile/presentation/widgets/profile_info_content.dart +++ b/lib/src/features/profile/presentation/widgets/profile_info_content.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/src/features/profile/domain/utils/event_sorter.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/providers/profile_controller_provider.dart'; +import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/profile_events_section.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/profile_stats.dart'; class ProfileInfoContent extends ConsumerWidget { @@ -12,6 +14,9 @@ class ProfileInfoContent extends ConsumerWidget { if (user == null) return const SizedBox(); + final sortedEvents = [...user.events]; + sortEvents(sortedEvents); + return Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -20,15 +25,14 @@ class ProfileInfoContent extends ConsumerWidget { user.lastName?.trim().isNotEmpty == true ? '${user.firstName} ${user.lastName}' : user.firstName, - style: Theme.of(context) - .textTheme - .headlineSmall - ?.copyWith(fontWeight: FontWeight.bold), + style: Theme.of( + context, + ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), ), const SizedBox(height: 24), ProfileStats(), - + ProfileEventsSection(events: sortedEvents), ], ); } diff --git a/lib/src/shared/domain/models/user_model.dart b/lib/src/shared/domain/models/user_model.dart index b69dbbe..ae410f8 100644 --- a/lib/src/shared/domain/models/user_model.dart +++ b/lib/src/shared/domain/models/user_model.dart @@ -1,3 +1,4 @@ +import 'package:interns2025b_mobile/src/shared/domain/models/event_model.dart'; import 'package:interns2025b_mobile/src/shared/domain/models/event_owner.dart'; class User implements EventOwner { @@ -13,6 +14,7 @@ class User implements EventOwner { final int eventsCount; final int followersCount; final int followingCount; + final List events; User({ required this.id, @@ -26,6 +28,7 @@ class User implements EventOwner { required this.eventsCount, required this.followersCount, required this.followingCount, + required this.events, }); factory User.fromJson(Map json) { @@ -47,6 +50,9 @@ class User implements EventOwner { eventsCount: json['events_count'] ?? 0, followersCount: json['followers_count'] ?? 0, followingCount: json['following_count'] ?? 0, + events: (json['events'] as List? ?? []) + .map((e) => Event.fromJson(e as Map)) + .toList(), ); } diff --git a/lib/src/shared/presentation/theme/app_colors.dart b/lib/src/shared/presentation/theme/app_colors.dart index aa95de3..c1beb09 100644 --- a/lib/src/shared/presentation/theme/app_colors.dart +++ b/lib/src/shared/presentation/theme/app_colors.dart @@ -28,4 +28,5 @@ class AppColors { static const borderEnable = Color(0x29687182); static const borderFill = Color(0xFFF5F5F5); static const red = Color(0xFFF44336); + static const draftEventBadge = Color(0xFFFFEB3B); } From dc9632ac9a6f9d1c8281e2b36782b4cc52075049 Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Tue, 29 Jul 2025 11:37:12 +0200 Subject: [PATCH 09/16] feat: Navigate to event details on profile event card tap --- .../presentation/widgets/profile_events_section.dart | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/src/features/profile/presentation/widgets/profile_events_section.dart b/lib/src/features/profile/presentation/widgets/profile_events_section.dart index 508a001..0c72778 100644 --- a/lib/src/features/profile/presentation/widgets/profile_events_section.dart +++ b/lib/src/features/profile/presentation/widgets/profile_events_section.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; +import 'package:interns2025b_mobile/src/core/routes/app_routes.dart'; import 'package:interns2025b_mobile/src/features/profile/presentation/widgets/profile_event_card.dart'; import 'package:interns2025b_mobile/src/shared/domain/models/event_model.dart'; @@ -34,7 +35,16 @@ class ProfileEventsSection extends ConsumerWidget { separatorBuilder: (_, _) => const SizedBox(width: 12), itemBuilder: (context, index) { final event = events[index]; - return ProfileEventCard(event: event); + return GestureDetector( + onTap: () { + Navigator.pushNamed( + context, + AppRoutes.eventDetails, + arguments: event.id, + ); + }, + child: ProfileEventCard(event: event), + ); }, ), ), From 4078526441f4f6d96497be0cb40482b77de85339 Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Tue, 29 Jul 2025 11:41:19 +0200 Subject: [PATCH 10/16] Fix: Adjust max width of profile event card --- .../profile/presentation/widgets/profile_event_card.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/features/profile/presentation/widgets/profile_event_card.dart b/lib/src/features/profile/presentation/widgets/profile_event_card.dart index ba59d3c..de9a48f 100644 --- a/lib/src/features/profile/presentation/widgets/profile_event_card.dart +++ b/lib/src/features/profile/presentation/widgets/profile_event_card.dart @@ -13,7 +13,7 @@ class ProfileEventCard extends StatelessWidget { @override Widget build(BuildContext context) { return ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 200), + constraints: const BoxConstraints(maxWidth: 210), child: Card( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), clipBehavior: Clip.antiAlias, From 351647c2c9f727633daf5924fb4dc0dcdcfb7e07 Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Tue, 29 Jul 2025 12:15:44 +0200 Subject: [PATCH 11/16] Refactor: Replace Colors with AppColors --- .../auth/presentation/controllers/auth_controller.dart | 3 ++- .../presentation/widgets/delete_user_request_button.dart | 2 +- lib/src/shared/presentation/theme/app_theme.dart | 2 +- lib/src/shared/presentation/widgets/button.dart | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/features/auth/presentation/controllers/auth_controller.dart b/lib/src/features/auth/presentation/controllers/auth_controller.dart index fb0708a..8a7694e 100644 --- a/lib/src/features/auth/presentation/controllers/auth_controller.dart +++ b/lib/src/features/auth/presentation/controllers/auth_controller.dart @@ -17,6 +17,7 @@ import 'package:interns2025b_mobile/src/features/auth/domain/usecases/login_usec import 'package:interns2025b_mobile/src/features/auth/domain/usecases/logout_usecase.dart'; import 'package:interns2025b_mobile/src/features/auth/domain/usecases/register_usecase.dart'; import 'package:interns2025b_mobile/src/shared/domain/models/user_model.dart'; +import 'package:interns2025b_mobile/src/shared/presentation/theme/app_colors.dart'; import 'package:shared_preferences/shared_preferences.dart'; class AuthController extends AsyncNotifier { @@ -160,7 +161,7 @@ class AuthController extends AsyncNotifier { void _showError(BuildContext context, String message) { ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(message), backgroundColor: Colors.red), + SnackBar(content: Text(message), backgroundColor: AppColors.red), ); } diff --git a/lib/src/features/profile/presentation/widgets/delete_user_request_button.dart b/lib/src/features/profile/presentation/widgets/delete_user_request_button.dart index 65c767b..9241c8c 100644 --- a/lib/src/features/profile/presentation/widgets/delete_user_request_button.dart +++ b/lib/src/features/profile/presentation/widgets/delete_user_request_button.dart @@ -41,7 +41,7 @@ class DeleteUserRequestButton extends ConsumerWidget { }, child: Text( localizations.confirm, - style: const TextStyle(color: Colors.red), + style: TextStyle(color: AppColors.red), ), ), ], diff --git a/lib/src/shared/presentation/theme/app_theme.dart b/lib/src/shared/presentation/theme/app_theme.dart index 92ac637..3a39cfe 100644 --- a/lib/src/shared/presentation/theme/app_theme.dart +++ b/lib/src/shared/presentation/theme/app_theme.dart @@ -42,7 +42,7 @@ class AppTheme { timePickerTheme: TimePickerThemeData( hourMinuteColor: WidgetStateColor.resolveWith((Set states) { if (states.contains(WidgetState.selected)) { - return Colors.green.withValues(alpha: 0.3); + return AppColors.greenLightLabelBackground.withValues(alpha: 0.3); } return Colors.transparent; }), diff --git a/lib/src/shared/presentation/widgets/button.dart b/lib/src/shared/presentation/widgets/button.dart index 0a838bb..b0eea65 100644 --- a/lib/src/shared/presentation/widgets/button.dart +++ b/lib/src/shared/presentation/widgets/button.dart @@ -19,8 +19,8 @@ class Button extends StatelessWidget { this.fullWidth = false, this.icon, this.isLoading = false, - this.backgroundColor = Colors.black, - this.foregroundColor = Colors.white, + this.backgroundColor = AppColors.black, + this.foregroundColor = AppColors.backgroundLight, this.iconColor, this.isOutlined = false, }); From dffd90ae772f5194b0daa7ae317da7c29b0e3134 Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Tue, 29 Jul 2025 12:36:50 +0200 Subject: [PATCH 12/16] Fix: Center profile info and display last name on new line --- .../widgets/profile_info_content.dart | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/src/features/profile/presentation/widgets/profile_info_content.dart b/lib/src/features/profile/presentation/widgets/profile_info_content.dart index 5afe5d3..055f93a 100644 --- a/lib/src/features/profile/presentation/widgets/profile_info_content.dart +++ b/lib/src/features/profile/presentation/widgets/profile_info_content.dart @@ -22,13 +22,21 @@ class ProfileInfoContent extends ConsumerWidget { children: [ const SizedBox(height: 24), Text( - user.lastName?.trim().isNotEmpty == true - ? '${user.firstName} ${user.lastName}' - : user.firstName, - style: Theme.of( - context, - ).textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold), + user.firstName, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, ), + if ((user.lastName?.trim().isNotEmpty ?? false)) + Text( + user.lastName!, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + + ), const SizedBox(height: 24), ProfileStats(), From 02c417ee6f1b7edac94fea4bf55dbaf33133cb51 Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Wed, 30 Jul 2025 09:46:47 +0200 Subject: [PATCH 13/16] localize: Update age category and event creation page --- .../event/presentation/pages/event_creation_page.dart | 4 +++- lib/src/features/event/presentation/widgets/event_card.dart | 3 ++- lib/src/shared/domain/models/age_category.dart | 5 +++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/src/features/event/presentation/pages/event_creation_page.dart b/lib/src/features/event/presentation/pages/event_creation_page.dart index 27d47e4..1adc03b 100644 --- a/lib/src/features/event/presentation/pages/event_creation_page.dart +++ b/lib/src/features/event/presentation/pages/event_creation_page.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; import 'package:interns2025b_mobile/src/shared/presentation/widgets/navigation_bar.dart'; import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_creation_form.dart'; @@ -43,9 +44,10 @@ class _EventCreationPageState extends ConsumerState { @override Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; return Scaffold( appBar: AppBar( - title: const Text('Create Event'), + title: Text(localizations.createEventButton), ), body: SingleChildScrollView( padding: const EdgeInsets.all(16), diff --git a/lib/src/features/event/presentation/widgets/event_card.dart b/lib/src/features/event/presentation/widgets/event_card.dart index 918c0ca..4ed961a 100644 --- a/lib/src/features/event/presentation/widgets/event_card.dart +++ b/lib/src/features/event/presentation/widgets/event_card.dart @@ -3,6 +3,7 @@ import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_data_time_row.dart'; import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_image.dart'; import 'package:interns2025b_mobile/src/features/event/presentation/widgets/event_price_tag.dart'; +import 'package:interns2025b_mobile/src/shared/domain/models/age_category.dart'; import 'package:interns2025b_mobile/src/shared/domain/models/event_model.dart'; import 'package:interns2025b_mobile/src/shared/presentation/theme/app_colors.dart'; @@ -61,7 +62,7 @@ class EventCard extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( - event.ageCategory!, + AgeCategoryX.fromString(event.ageCategory).label(context), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.shadeGrey700, ), diff --git a/lib/src/shared/domain/models/age_category.dart b/lib/src/shared/domain/models/age_category.dart index 72fbcf4..f05ee67 100644 --- a/lib/src/shared/domain/models/age_category.dart +++ b/lib/src/shared/domain/models/age_category.dart @@ -18,9 +18,10 @@ extension AgeCategoryX on AgeCategory { } } - static AgeCategory? fromLabel(BuildContext context, String label) { + static AgeCategory fromString(String? value) { + if (value == null) return AgeCategory.everyone; return AgeCategory.values.firstWhere( - (e) => e.label(context) == label, + (e) => e.name == value.toLowerCase(), orElse: () => AgeCategory.everyone, ); } From 1f4266886722c739500c5c96ba7ef4b181d3f127 Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Wed, 30 Jul 2025 13:04:58 +0200 Subject: [PATCH 14/16] Refactor: Use const for widgets and update AgeCategory localization --- .../presentation/widgets/event_card.dart | 2 +- .../event_form/age_category_dropdown.dart | 2 +- .../widgets/event_status_badge.dart | 2 +- .../widgets/profile_info_content.dart | 4 +-- .../presentation/widgets/profile_stats.dart | 1 - .../shared/domain/models/age_category.dart | 30 +++++++++---------- 6 files changed, 18 insertions(+), 23 deletions(-) diff --git a/lib/src/features/event/presentation/widgets/event_card.dart b/lib/src/features/event/presentation/widgets/event_card.dart index 4ed961a..bdd4b51 100644 --- a/lib/src/features/event/presentation/widgets/event_card.dart +++ b/lib/src/features/event/presentation/widgets/event_card.dart @@ -62,7 +62,7 @@ class EventCard extends StatelessWidget { Padding( padding: const EdgeInsets.only(top: 4.0), child: Text( - AgeCategoryX.fromString(event.ageCategory).label(context), + AgeCategory.fromString(event.ageCategory).localized(context), style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.shadeGrey700, ), diff --git a/lib/src/features/event/presentation/widgets/event_form/age_category_dropdown.dart b/lib/src/features/event/presentation/widgets/event_form/age_category_dropdown.dart index e11cb70..b6d550c 100644 --- a/lib/src/features/event/presentation/widgets/event_form/age_category_dropdown.dart +++ b/lib/src/features/event/presentation/widgets/event_form/age_category_dropdown.dart @@ -27,7 +27,7 @@ class AgeCategoryDropdown extends StatelessWidget { items: AgeCategory.values.map((category) { return DropdownMenuItem( value: category, - child: Text(category.label(context)), + child: Text(category.localized(context)), ); }).toList(), onChanged: onChanged, diff --git a/lib/src/features/profile/presentation/widgets/event_status_badge.dart b/lib/src/features/profile/presentation/widgets/event_status_badge.dart index 17b8a45..d351e4e 100644 --- a/lib/src/features/profile/presentation/widgets/event_status_badge.dart +++ b/lib/src/features/profile/presentation/widgets/event_status_badge.dart @@ -32,7 +32,7 @@ class EventStatusBadge extends StatelessWidget { ), child: Text( status.label(context), - style: TextStyle( + style: const TextStyle( color: AppColors.black, fontSize: 12, fontWeight: FontWeight.w500, diff --git a/lib/src/features/profile/presentation/widgets/profile_info_content.dart b/lib/src/features/profile/presentation/widgets/profile_info_content.dart index 055f93a..084a3d1 100644 --- a/lib/src/features/profile/presentation/widgets/profile_info_content.dart +++ b/lib/src/features/profile/presentation/widgets/profile_info_content.dart @@ -35,11 +35,9 @@ class ProfileInfoContent extends ConsumerWidget { fontWeight: FontWeight.bold, ), textAlign: TextAlign.center, - ), const SizedBox(height: 24), - - ProfileStats(), + const ProfileStats(), ProfileEventsSection(events: sortedEvents), ], ); diff --git a/lib/src/features/profile/presentation/widgets/profile_stats.dart b/lib/src/features/profile/presentation/widgets/profile_stats.dart index 98187d9..a81385f 100644 --- a/lib/src/features/profile/presentation/widgets/profile_stats.dart +++ b/lib/src/features/profile/presentation/widgets/profile_stats.dart @@ -28,7 +28,6 @@ class ProfileStats extends ConsumerWidget { label: localizations.followingCount, value: user.followingCount, ), - const SizedBox(height: 16), ], ); } diff --git a/lib/src/shared/domain/models/age_category.dart b/lib/src/shared/domain/models/age_category.dart index f05ee67..220ea70 100644 --- a/lib/src/shared/domain/models/age_category.dart +++ b/lib/src/shared/domain/models/age_category.dart @@ -1,27 +1,25 @@ import 'package:flutter/material.dart'; import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; -enum AgeCategory { everyone, adults, youth, children } +enum AgeCategory { + everyone, + adults, + youth, + children; -extension AgeCategoryX on AgeCategory { - String label(BuildContext context) { - final localizations = AppLocalizations.of(context)!; - switch (this) { - case AgeCategory.everyone: - return localizations.ageEveryone; - case AgeCategory.adults: - return localizations.ageAdults; - case AgeCategory.youth: - return localizations.ageTeens; - case AgeCategory.children: - return localizations.ageKids; - } + String localized(BuildContext context) { + final localization = AppLocalizations.of(context)!; + return { + AgeCategory.everyone: localization.ageEveryone, + AgeCategory.adults: localization.ageAdults, + AgeCategory.youth: localization.ageTeens, + AgeCategory.children: localization.ageKids, + }[this]!; } static AgeCategory fromString(String? value) { - if (value == null) return AgeCategory.everyone; return AgeCategory.values.firstWhere( - (e) => e.name == value.toLowerCase(), + (e) => e.name.toLowerCase() == value?.toLowerCase(), orElse: () => AgeCategory.everyone, ); } From 4655ea4b7fb11e402baafaaaa2cfc9fe0754c1c4 Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Wed, 30 Jul 2025 13:19:31 +0200 Subject: [PATCH 15/16] Refactor: Convert enum extensions to instance/static methods --- lib/src/shared/domain/models/age_category.dart | 18 +++++++++++------- lib/src/shared/domain/models/event_model.dart | 4 ++-- lib/src/shared/domain/models/event_status.dart | 8 ++++++-- lib/src/shared/domain/models/navbar_pages.dart | 6 ++++-- lib/src/shared/domain/models/owner_type.dart | 9 +++++---- 5 files changed, 28 insertions(+), 17 deletions(-) diff --git a/lib/src/shared/domain/models/age_category.dart b/lib/src/shared/domain/models/age_category.dart index 220ea70..53c0af4 100644 --- a/lib/src/shared/domain/models/age_category.dart +++ b/lib/src/shared/domain/models/age_category.dart @@ -9,17 +9,21 @@ enum AgeCategory { String localized(BuildContext context) { final localization = AppLocalizations.of(context)!; - return { - AgeCategory.everyone: localization.ageEveryone, - AgeCategory.adults: localization.ageAdults, - AgeCategory.youth: localization.ageTeens, - AgeCategory.children: localization.ageKids, - }[this]!; + switch (this) { + case AgeCategory.everyone: + return localization.ageEveryone; + case AgeCategory.adults: + return localization.ageAdults; + case AgeCategory.youth: + return localization.ageTeens; + case AgeCategory.children: + return localization.ageKids; + } } static AgeCategory fromString(String? value) { return AgeCategory.values.firstWhere( - (e) => e.name.toLowerCase() == value?.toLowerCase(), + (e) => e.name.toLowerCase() == value?.toLowerCase(), orElse: () => AgeCategory.everyone, ); } diff --git a/lib/src/shared/domain/models/event_model.dart b/lib/src/shared/domain/models/event_model.dart index 934bc3c..2597e90 100644 --- a/lib/src/shared/domain/models/event_model.dart +++ b/lib/src/shared/domain/models/event_model.dart @@ -49,7 +49,7 @@ class Event { factory Event.fromJson(Map json) { final rawOwnerType = json['owner_type'] as String?; - final ownerType = OwnerTypeX.fromRaw(rawOwnerType); + final ownerType = OwnerType.fromRaw(rawOwnerType); final ownerJson = json['owner']; EventOwner? parsedOwner; @@ -80,7 +80,7 @@ class Event { : null, isPaid: json['is_paid'], price: json['price'] != null ? (json['price'] as num).toDouble() : null, - status: EventStatusX.fromString(json['status']), + status: EventStatus.fromString(json['status']), imageUrl: json['image_url'], ageCategory: json['age_category'], ownerType: ownerType, diff --git a/lib/src/shared/domain/models/event_status.dart b/lib/src/shared/domain/models/event_status.dart index 45aba6c..2b84e34 100644 --- a/lib/src/shared/domain/models/event_status.dart +++ b/lib/src/shared/domain/models/event_status.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; -enum EventStatus { draft, published, ongoing, ended, canceled } +enum EventStatus { + draft, + published, + ongoing, + ended, + canceled; -extension EventStatusX on EventStatus { String label(BuildContext context) { final localizations = AppLocalizations.of(context)!; switch (this) { diff --git a/lib/src/shared/domain/models/navbar_pages.dart b/lib/src/shared/domain/models/navbar_pages.dart index 7527064..ec0b604 100644 --- a/lib/src/shared/domain/models/navbar_pages.dart +++ b/lib/src/shared/domain/models/navbar_pages.dart @@ -2,9 +2,11 @@ import 'package:flutter/material.dart'; import 'package:interns2025b_mobile/l10n/generated/app_localizations.dart'; import 'package:interns2025b_mobile/src/core/routes/app_routes.dart'; -enum NavbarPages { events, addEvent, profile } +enum NavbarPages { + events, + addEvent, + profile; -extension NavbarPagesExtension on NavbarPages { String get routeName { switch (this) { case NavbarPages.events: diff --git a/lib/src/shared/domain/models/owner_type.dart b/lib/src/shared/domain/models/owner_type.dart index 5cbd96f..2f0e3b6 100644 --- a/lib/src/shared/domain/models/owner_type.dart +++ b/lib/src/shared/domain/models/owner_type.dart @@ -1,13 +1,14 @@ -enum OwnerType { user, organization } +enum OwnerType { + user, + organization; -extension OwnerTypeX on OwnerType { static OwnerType fromRaw(String? value) { switch (value?.toLowerCase()) { case 'user': - case 'interns2025b\\models\\user': + case 'Interns2025b\\models\\user': return OwnerType.user; case 'organization': - case 'interns2025b\\models\\organization': + case 'Interns2025b\\models\\organization': return OwnerType.organization; default: throw ArgumentError('Unknown owner type: $value'); From a6315f2a8a750512607c0d89547d0d7e8e6f54e3 Mon Sep 17 00:00:00 2001 From: Dominik Prabucki Date: Wed, 30 Jul 2025 13:25:27 +0200 Subject: [PATCH 16/16] Refactor: Rename variable for clarity in localization --- lib/src/shared/domain/models/age_category.dart | 10 +++++----- lib/src/shared/domain/models/owner_type.dart | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/src/shared/domain/models/age_category.dart b/lib/src/shared/domain/models/age_category.dart index 53c0af4..4c69c92 100644 --- a/lib/src/shared/domain/models/age_category.dart +++ b/lib/src/shared/domain/models/age_category.dart @@ -8,16 +8,16 @@ enum AgeCategory { children; String localized(BuildContext context) { - final localization = AppLocalizations.of(context)!; + final localizations = AppLocalizations.of(context)!; switch (this) { case AgeCategory.everyone: - return localization.ageEveryone; + return localizations.ageEveryone; case AgeCategory.adults: - return localization.ageAdults; + return localizations.ageAdults; case AgeCategory.youth: - return localization.ageTeens; + return localizations.ageTeens; case AgeCategory.children: - return localization.ageKids; + return localizations.ageKids; } } diff --git a/lib/src/shared/domain/models/owner_type.dart b/lib/src/shared/domain/models/owner_type.dart index 2f0e3b6..f6a3c7a 100644 --- a/lib/src/shared/domain/models/owner_type.dart +++ b/lib/src/shared/domain/models/owner_type.dart @@ -5,10 +5,10 @@ enum OwnerType { static OwnerType fromRaw(String? value) { switch (value?.toLowerCase()) { case 'user': - case 'Interns2025b\\models\\user': + case 'interns2025b\\models\\user': return OwnerType.user; case 'organization': - case 'Interns2025b\\models\\organization': + case 'interns2025b\\models\\organization': return OwnerType.organization; default: throw ArgumentError('Unknown owner type: $value');