diff --git a/frontend/app/lib/main.dart b/frontend/app/lib/main.dart index 6b36af7..42bb5d6 100644 --- a/frontend/app/lib/main.dart +++ b/frontend/app/lib/main.dart @@ -1,11 +1,15 @@ import 'package:app/pages/home_page.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart' + show GlobalMaterialLocalizations; void main() async { WidgetsFlutterBinding.ensureInitialized(); runApp( MaterialApp( + localizationsDelegates: const [GlobalMaterialLocalizations.delegate], + supportedLocales: const [Locale('en'), Locale('de')], theme: ThemeData().copyWith( colorScheme: const ColorScheme( brightness: Brightness.dark, diff --git a/frontend/app/lib/model/apis/app_exception.dart b/frontend/app/lib/model/apis/app_exception.dart index 7093126..2fa3844 100644 --- a/frontend/app/lib/model/apis/app_exception.dart +++ b/frontend/app/lib/model/apis/app_exception.dart @@ -4,6 +4,7 @@ class AppException implements Exception { AppException([this._message, this._prefix]); + @override String toString() { return "$_prefix$_message"; } diff --git a/frontend/app/lib/model/services/backend_service.dart b/frontend/app/lib/model/services/backend_service.dart index af38df8..a0c8cb8 100644 --- a/frontend/app/lib/model/services/backend_service.dart +++ b/frontend/app/lib/model/services/backend_service.dart @@ -14,6 +14,7 @@ import 'package:app/pb/rpc_get_person.pb.dart'; import 'package:app/pb/rpc_list_persons.pb.dart'; import 'package:app/pb/rpc_login.pb.dart'; import 'package:app/pb/rpc_refresh_token.pb.dart'; +import 'package:app/pb/rpc_update_person.pb.dart'; import 'package:app/pb/service_df.pbgrpc.dart'; import 'package:fixnum/fixnum.dart'; import 'package:grpc/grpc.dart'; @@ -114,6 +115,10 @@ class BackendService { return true; } + static Future get accountId async { + return (await Session.session).accountId; + } + static Future createAccount( {required String email, required String password}) async { try { @@ -207,14 +212,15 @@ class BackendService { } } - Future createPerson( - {required String firstname, - required String lastname, - required String street, - required String zip, - required String city, - required String country, - required DateTime birthday}) async { + Future updatePerson( + {required Int64 id, + String? firstname, + String? lastname, + String? street, + String? zip, + String? city, + String? country, + Timestamp? birthday}) async { Session session = await Session.session; if (session.accessTokenExpiresAt == null) { throw UnauthorizedException('Keine Siztung gefunden'); @@ -226,16 +232,74 @@ class BackendService { } } try { - final CreatePersonResponse response = await _client.createPerson( - CreatePersonRequest( - accountId: session.accountId, - lastname: lastname, - firstname: firstname, - street: street, - zip: zip, - country: country, - birthday: Timestamp.fromDateTime(birthday), - ), + final UpdatePersonRequest req = UpdatePersonRequest( + id: id, + ); + + if (lastname != null) { + req.lastname = lastname; + } + if (firstname != null) { + req.firstname = firstname; + } + if (street != null) { + req.street = street; + } + if (city != null) { + req.city = city; + } + if (zip != null) { + req.zip = zip; + } + if (country != null) { + req.country = country; + } + if (birthday != null) { + req.birthday = birthday; + } + final UpdatePersonResponse response = await _client.updatePerson(req, + options: CallOptions( + metadata: {'Authorization': 'Bearer ${session.accessToken}'})); + return response.person; + } on SocketException { + throw FetchDataException('Keine Internet Verbindung'); + } on GrpcError catch (err) { + throw FetchDataException(err.message); + } catch (err) { + throw InternalException(err.toString()); + } + } + + Future createPerson( + {required String firstname, + required String lastname, + required String street, + required String zip, + required String city, + required String country, + required Timestamp birthday}) async { + Session session = await Session.session; + if (session.accessTokenExpiresAt == null) { + throw UnauthorizedException('Keine Siztung gefunden'); + } + if (session.accessTokenExpiresAt!.toDateTime().isBefore(DateTime.now())) { + session = await refreshToken(session); + if (session.accessTokenExpiresAt == null) { + throw UnauthorizedException('Sitzung ist abgelaufen'); + } + } + try { + final CreatePersonRequest req = CreatePersonRequest( + accountId: session.accountId, + lastname: lastname, + firstname: firstname, + street: street, + city: city, + zip: zip, + country: country, + birthday: birthday, + ); + final CreatePersonResponse response = await _client.createPerson(req, options: CallOptions( metadata: {'Authorization': 'Bearer ${session.accessToken}'})); return response.person; diff --git a/frontend/app/lib/model/view_model/account_vm.dart b/frontend/app/lib/model/view_model/account_vm.dart index fb3a998..1457964 100644 --- a/frontend/app/lib/model/view_model/account_vm.dart +++ b/frontend/app/lib/model/view_model/account_vm.dart @@ -7,11 +7,12 @@ class AccountViewModel extends BaseViewModel { AccountViewModel() { _init(); } - ApiResponse _apiResponse = ApiResponse.initial('Keine Daten'); + final ApiResponse _apiResponse = ApiResponse.initial('Keine Daten'); final BackendService _service = BackendService(); Account? _account; + @override ApiResponse get response { return _apiResponse; } diff --git a/frontend/app/lib/model/view_model/persons_vm.dart b/frontend/app/lib/model/view_model/persons_vm.dart index a8ed578..f3f5a9e 100644 --- a/frontend/app/lib/model/view_model/persons_vm.dart +++ b/frontend/app/lib/model/view_model/persons_vm.dart @@ -1,6 +1,9 @@ import 'package:app/model/apis/api_response.dart'; import 'package:app/model/services/backend_service.dart'; +import 'package:app/pb/google/protobuf/timestamp.pb.dart'; import 'package:app/pb/person.pb.dart'; +import 'package:app/util/colors.dart'; +import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; class PersonsViewModel with ChangeNotifier { @@ -35,8 +38,9 @@ class PersonsViewModel with ChangeNotifier { required String zip, required String city, required String country, - required DateTime birthday}) async { + required Timestamp birthday}) async { Person person = Person(); + final messenger = ScaffoldMessenger.of(context); _apiResponse = ApiResponse.loading('Erstelle Person'); try { person = await _service.createPerson( @@ -47,8 +51,102 @@ class PersonsViewModel with ChangeNotifier { city: city, country: country, birthday: birthday); + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.success, + content: const Text( + 'Gepeichert', + style: TextStyle(color: Colors.white), + ), + )); _apiResponse = ApiResponse.completed(person); } catch (err) { + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.error, + content: const Text( + 'Fehler beim Speichern', + style: TextStyle(color: Colors.white), + ), + action: SnackBarAction( + label: 'Details', + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.black, + icon: Icon( + Icons.error, + color: CustomColors.error, + ), + content: Text( + err.toString(), + textAlign: TextAlign.center, + ), + )); + }, + ), + )); + _apiResponse = ApiResponse.error(err.toString()); + } + notifyListeners(); + return person; + } + + Future updatePerson(BuildContext context, + {required Int64 id, + String? firstname, + String? lastname, + String? street, + String? zip, + String? city, + String? country, + Timestamp? birthday}) async { + Person person = Person(); + final messenger = ScaffoldMessenger.of(context); + _apiResponse = ApiResponse.loading('Erstelle Person'); + try { + person = await _service.updatePerson( + id: id, + firstname: firstname, + lastname: lastname, + street: street, + zip: zip, + city: city, + country: country, + birthday: birthday); + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.success, + content: const Text( + 'Gepeichert', + style: TextStyle(color: Colors.white), + ), + )); + _apiResponse = ApiResponse.completed(person); + } catch (err) { + messenger.showSnackBar(SnackBar( + backgroundColor: CustomColors.error, + content: const Text( + 'Fehler beim Speichern', + style: TextStyle(color: Colors.white), + ), + action: SnackBarAction( + label: 'Details', + onPressed: () { + showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: Colors.black, + icon: Icon( + Icons.error, + color: CustomColors.error, + ), + content: Text( + err.toString(), + textAlign: TextAlign.center, + ), + )); + }, + ), + )); _apiResponse = ApiResponse.error(err.toString()); } notifyListeners(); diff --git a/frontend/app/lib/pages/person_details_page.dart b/frontend/app/lib/pages/person_details_page.dart new file mode 100644 index 0000000..86f38b6 --- /dev/null +++ b/frontend/app/lib/pages/person_details_page.dart @@ -0,0 +1,339 @@ +import 'package:app/model/view_model/persons_vm.dart'; +import 'package:app/pb/google/protobuf/timestamp.pb.dart'; +import 'package:app/pb/person.pb.dart'; +import 'package:app/util/validation.dart'; +import 'package:app/widgets/background.dart'; +import 'package:app/widgets/bottom_navigation.dart'; +import 'package:app/widgets/bottom_navigation_item.dart'; +import 'package:fixnum/fixnum.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:intl/intl.dart'; + +Future showPerson(BuildContext context, {Person? person}) async { + PersonsViewModel vm = PersonsViewModel(); + + final formKey = GlobalKey(); + final firstnameController = TextEditingController(); + final lastnameController = TextEditingController(); + final cityController = TextEditingController(); + final zipController = TextEditingController(); + final streetController = TextEditingController(); + final countryController = TextEditingController(); + final birthdayController = TextEditingController(); + + Future _init() async { + if (person == null) { + person ??= Person(); + // person ??= Person(accountId: await BackendService.accountId); + } else { + firstnameController.text = person!.firstname; + lastnameController.text = person!.lastname; + cityController.text = person!.city; + zipController.text = person!.zip; + streetController.text = person!.street; + countryController.text = person!.country; + birthdayController.text = + DateFormat('dd.MM.yyyy').format(person!.birthday.toDateTime()); + } + } + + await _init(); + + void _updateData() { + person!.firstname = firstnameController.text; + person!.lastname = lastnameController.text; + person!.city = cityController.text; + person!.street = streetController.text; + person!.zip = zipController.text; + person!.country = countryController.text; + } + + Future createPerson(BuildContext context) async { + final navigator = Navigator.of(context); + _updateData(); + person!.id = Int64(0); + person = await vm.createPerson(context, + firstname: person!.firstname, + lastname: person!.lastname, + street: person!.street, + zip: person!.zip, + city: person!.city, + country: person!.country, + birthday: person!.birthday); + + if (person!.id != 0) { + navigator.pop(person); + } + } + + Future updatePerson(BuildContext context) async { + final navigator = Navigator.of(context); + _updateData(); + final personUpdate = await vm.updatePerson(context, + id: person!.id, + firstname: person!.firstname != firstnameController.text + ? person!.firstname + : null, + lastname: person!.lastname != lastnameController.text + ? person!.lastname + : null, + street: person!.street != streetController.text ? person!.street : null, + zip: person!.zip != zipController.text ? person!.zip : null, + city: person!.city != cityController.text ? person!.city : null, + country: + person!.country != countryController.text ? person!.country : null, + birthday: + DateFormat('dd.MM.yyyy').format(person!.birthday.toDateTime()) != + birthdayController.text + ? person!.birthday + : null); + + if (personUpdate != person) { + navigator.pop(person); + } + } + + // ignore: use_build_context_synchronously + await showModalBottomSheet( + context: context, + builder: (builder) { + return Background( + child: Scaffold( + bottomNavigationBar: BottomNavigation( + hideMenu: true, + children: [ + BottomNavigationItem( + onPressed: () { + Navigator.pop(context, false); + }, + icon: Icons.arrow_back, + color: Colors.white, + label: 'Zurück', + ), + BottomNavigationItem( + onPressed: () { + Navigator.pop(context, false); + }, + icon: Icons.home, + color: Colors.white, + label: 'Home', + ), + ], + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + height: 50, + ), + Text( + person!.id == 0 ? 'Person anlegen' : 'Person anpassen', + style: const TextStyle( + fontFamily: 'sans-serif', + fontSize: 24, + height: 1.6, + fontWeight: FontWeight.normal, + letterSpacing: 6), + ), + ChangeNotifierProvider( + create: (context) => PersonsViewModel(), + child: Consumer( + builder: (context, value, child) => Form( + key: formKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox( + height: 40, + ), + TextFormField( + controller: firstnameController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('Vorname'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Vorname', + ), + keyboardType: TextInputType.name, + validator: (value) { + if (value == null || !value.isValidName) { + return 'Bitte einen gültigen Vornamen eingeben'; + } + return null; + }, + ), + TextFormField( + controller: lastnameController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('Nachname'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Nachname', + ), + keyboardType: TextInputType.name, + validator: (value) { + if (value == null || !value.isValidName) { + return 'Bitte einen gültigen Nachnamen eingeben'; + } + return null; + }, + ), + TextFormField( + readOnly: true, + onTap: () async { + DateTime? pickedDate = await showDatePicker( + context: context, + locale: const Locale('de', 'DE'), + initialDate: DateTime.now(), + firstDate: DateTime(1930), + lastDate: DateTime(DateTime.now().year + 1), + builder: (context, child) => Theme( + data: ThemeData.dark(), + child: child != null ? child : Text(''), + ), + ); + + if (pickedDate != null) { + person!.birthday = + Timestamp.fromDateTime(pickedDate); + birthdayController.text = + DateFormat('dd.MM.yyyy') + .format(pickedDate); + } + }, + controller: birthdayController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('Geburtstag'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Geburtstag', + ), + keyboardType: TextInputType.name, + // validator: (value) { + // if (value == null || !value.isValidName) { + // return 'Bitte einen gültigen Nachnamen eingeben'; + // } + // return null; + // }, + ), + TextFormField( + controller: streetController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('Straße'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Straße mit Hausnummer', + ), + keyboardType: TextInputType.name, + validator: (value) { + if (value == null || !value.isValidName) { + return 'Bitte eine gültige Straße mit Hausnummer eingeben'; + } + return null; + }, + ), + TextFormField( + controller: zipController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('PLZ'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'PLZ', + ), + keyboardType: TextInputType.name, + validator: (value) { + if (value == null || !value.isValidName) { + return 'Bitte eine gültige PLZ eingeben'; + } + return null; + }, + ), + TextFormField( + controller: cityController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('Stadt'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Stadt', + ), + keyboardType: TextInputType.name, + validator: (value) { + if (value == null || !value.isValidName) { + return 'Bitte eine gültige Stadt eingeben'; + } + return null; + }, + ), + TextFormField( + controller: countryController, + decoration: const InputDecoration( + fillColor: Color.fromARGB(30, 255, 255, 255), + filled: true, + suffix: Text('Land'), + hintStyle: TextStyle( + color: Colors.white38, + ), + hintText: 'Land', + ), + keyboardType: TextInputType.name, + validator: (value) { + if (value == null || !value.isValidName) { + return 'Bitte ein gültiges Land eingeben'; + } + return null; + }, + ), + const SizedBox( + height: 15, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: () async { + person!.id.isZero + ? await createPerson(context) + : await updatePerson(context); + }, + child: const Icon(Icons.update), + ), + ], + ) + ], + ), + ), + ), + ), + ], + ), + ), + ), + ); + }, + useSafeArea: true, + isScrollControlled: true, + backgroundColor: Colors.black); + return person!; +} diff --git a/frontend/app/lib/pages/persons_page.dart b/frontend/app/lib/pages/persons_page.dart index d863dec..e16c89d 100644 --- a/frontend/app/lib/pages/persons_page.dart +++ b/frontend/app/lib/pages/persons_page.dart @@ -2,7 +2,9 @@ import 'package:app/model/apis/api_response.dart'; import 'package:app/model/services/backend_service.dart'; import 'package:app/model/view_model/persons_vm.dart'; import 'package:app/pages/home_page.dart'; +import 'package:app/pages/person_details_page.dart'; import 'package:app/pb/person.pb.dart'; +import 'package:app/util/validation.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_navigation.dart'; import 'package:app/widgets/bottom_navigation_item.dart'; @@ -61,13 +63,56 @@ class _PersonsPageState extends State { } List _personsList(List persons) { + persons.sort((a, b) { + final comp = a.lastname.compareTo(b.lastname); + if (comp != 0) { + return comp; + } + return a.firstname.compareTo(b.firstname); + }); final List list = []; for (var p in persons) { - list.add(Card( - color: Colors.black, - child: Text( - '${p.firstname} ${p.lastname}', - style: const TextStyle(color: Colors.white), + list.add(TextButton( + onPressed: () async { + final Person per = await showPerson(context, person: p); + setState(() { + this.persons.add(per); + }); + }, + child: Card( + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: const Color.fromARGB(100, 89, 88, 88), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 14), + child: Row( + children: [ + Container( + height: 40, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + p.lastname.titleCase, + style: const TextStyle(color: Colors.white), + // overflow: TextOverflow.fade, + textAlign: TextAlign.start, + ), + const Spacer(), + Text( + p.firstname.titleCase, + style: const TextStyle(color: Colors.white), + textAlign: TextAlign.start, + // overflow: TextOverflow.fade, + ), + ], + ), + ), + const Spacer(), + const Text('STATUS') + ], + ), + ), ), )); } @@ -80,7 +125,14 @@ class _PersonsPageState extends State { child: Scaffold( floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: FloatingActionButton( - onPressed: () {}, + onPressed: () async { + final p = await showPerson(context); + if (!p.id.isZero) { + setState(() { + persons.add(p); + }); + } + }, child: const Icon(Icons.add), ), appBar: AppBar( diff --git a/frontend/app/lib/pages_old/dashboard_page.dart b/frontend/app/lib/pages_old/dashboard_page.dart index 3c8503b..e012989 100644 --- a/frontend/app/lib/pages_old/dashboard_page.dart +++ b/frontend/app/lib/pages_old/dashboard_page.dart @@ -1,9 +1,5 @@ -import 'package:app/gapi/client.dart'; -import 'package:app/pages_old/start_page.dart'; import 'package:app/pb/account_info.pb.dart'; -import 'package:app/pb/rpc_get_account_info.pb.dart'; import 'package:app/widgets/background.dart'; -import 'package:app/widgets/bottom_bar.dart'; import 'package:app/widgets/loading_widget.dart'; import 'package:app/widgets/side_drawer.dart'; import 'package:flutter/material.dart'; diff --git a/frontend/app/lib/pages_old/login_page.dart b/frontend/app/lib/pages_old/login_page.dart index d2207e7..43923c2 100644 --- a/frontend/app/lib/pages_old/login_page.dart +++ b/frontend/app/lib/pages_old/login_page.dart @@ -1,12 +1,9 @@ -import 'package:app/gapi/client.dart'; import 'package:app/model/services/backend_service.dart'; -import 'package:app/pages_old/start_page.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_bar.dart'; import 'package:app/widgets/loading_widget.dart'; import 'package:app/widgets/side_drawer.dart'; import 'package:flutter/material.dart'; -import 'package:grpc/grpc.dart'; // GlobalKey scaffoldKey = GlobalKey(); diff --git a/frontend/app/lib/pages_old/register_page.dart b/frontend/app/lib/pages_old/register_page.dart index 2a40fd6..5532a09 100644 --- a/frontend/app/lib/pages_old/register_page.dart +++ b/frontend/app/lib/pages_old/register_page.dart @@ -1,11 +1,8 @@ -import 'package:app/gapi/client.dart'; -import 'package:app/pages_old/start_page.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_bar.dart'; import 'package:app/widgets/loading_widget.dart'; import 'package:app/widgets/side_drawer.dart'; import 'package:flutter/material.dart'; -import 'package:grpc/grpc.dart'; class RegisterPage extends StatefulWidget { const RegisterPage({ diff --git a/frontend/app/lib/pages_old/start_page.dart b/frontend/app/lib/pages_old/start_page.dart index 6235c2c..4eb160f 100644 --- a/frontend/app/lib/pages_old/start_page.dart +++ b/frontend/app/lib/pages_old/start_page.dart @@ -1,9 +1,6 @@ -import 'package:app/gapi/client.dart'; import 'package:app/model/apis/api_response.dart'; import 'package:app/model/view_model/account_vm.dart'; -import 'package:app/pages_old/dashboard_page.dart'; import 'package:app/pages_old/login_page.dart'; -import 'package:app/pages_old/register_page.dart'; import 'package:app/pb/account.pb.dart'; import 'package:app/widgets/background.dart'; import 'package:app/widgets/bottom_bar.dart'; diff --git a/frontend/app/lib/util/validation.dart b/frontend/app/lib/util/validation.dart index a7aa9e5..bc20152 100644 --- a/frontend/app/lib/util/validation.dart +++ b/frontend/app/lib/util/validation.dart @@ -24,4 +24,10 @@ extension valString on String { bool get isValidPhone { return phoneRegExp.hasMatch(this); } + + String get titleCase { + return split(' ') + .map((str) => str[0].toUpperCase() + str.substring(1)) + .join(' '); + } } diff --git a/frontend/app/lib/widgets/bottom_navigation.dart b/frontend/app/lib/widgets/bottom_navigation.dart index c3f6519..9f8b0d7 100644 --- a/frontend/app/lib/widgets/bottom_navigation.dart +++ b/frontend/app/lib/widgets/bottom_navigation.dart @@ -7,10 +7,13 @@ class BottomNavigation extends StatelessWidget { required this.children, this.backgroundColor, this.iconColor, + this.hideMenu, }) { + hideMenu ??= false; backgroundColor ??= Colors.black; } + bool? hideMenu; List children; Color? backgroundColor; Color? iconColor; @@ -32,14 +35,18 @@ class BottomNavigation extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ ...children, - Builder(builder: (context) { - return IconButton( - onPressed: () => Scaffold.of(context).openDrawer(), - icon: const Icon( - Icons.menu, - color: Colors.white, - )); - }), + if (!hideMenu!) + Builder( + builder: (context) { + return IconButton( + onPressed: () => Scaffold.of(context).openDrawer(), + icon: const Icon( + Icons.menu, + color: Colors.white, + ), + ); + }, + ) ], ), ), diff --git a/frontend/app/pubspec.lock b/frontend/app/pubspec.lock index 112b12d..8d5d6cc 100644 --- a/frontend/app/pubspec.lock +++ b/frontend/app/pubspec.lock @@ -110,6 +110,11 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_test: dependency: "direct dev" description: flutter @@ -155,6 +160,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + url: "https://pub.dev" + source: hosted + version: "0.18.1" js: dependency: transitive description: diff --git a/frontend/app/pubspec.yaml b/frontend/app/pubspec.yaml index 2960757..3f39504 100644 --- a/frontend/app/pubspec.yaml +++ b/frontend/app/pubspec.yaml @@ -33,6 +33,8 @@ dependencies: collection: ^1.15.0-nullsafety.4 flutter: sdk: flutter + flutter_localizations: + sdk: flutter # The following adds the Cupertino Icons font to your application. @@ -43,6 +45,7 @@ dependencies: path: ^1.8.3 fixnum: ^1.1.0 provider: ^6.0.5 + intl: ^0.18.1 dev_dependencies: lints: ^2.0.0