From 4c527af746ffdcfc259f9e148531ae587c02571b Mon Sep 17 00:00:00 2001 From: sBubshait Date: Tue, 22 Jul 2025 08:38:48 +0300 Subject: [PATCH] feat: invitation creation form --- frontend/lib/main.dart | 24 +- .../lib/screens/pages/invitations_page.dart | 490 +++++++++++++++++- 2 files changed, 495 insertions(+), 19 deletions(-) diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index d30af18..615882b 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -42,7 +42,7 @@ class _SplashScreenState extends State { await Future.delayed(Duration(milliseconds: 500)); final isLoggedIn = await AuthService.isLoggedIn(); - + if (isLoggedIn) { final userResult = await UserService.getCurrentUser(); if (userResult['success'] == true) { @@ -56,9 +56,9 @@ class _SplashScreenState extends State { ); } } else { - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => LandingPage()), - ); + Navigator.of( + context, + ).pushReplacement(MaterialPageRoute(builder: (context) => LandingPage())); } } @@ -70,10 +70,7 @@ class _SplashScreenState extends State { gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [ - Color(0xFF32B0A5), - Color(0xFF4600B9), - ], + colors: [Color(0xFF32B0A5), Color(0xFF4600B9)], stops: [0.0, 0.5], ), ), @@ -334,9 +331,11 @@ class _SignInPageState extends State { if (result['success'] == true) { final userResult = await UserService.getCurrentUser(forceRefresh: true); - + Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => NotificationPermissionScreen()), + MaterialPageRoute( + builder: (context) => NotificationPermissionScreen(), + ), ); } else { _showErrorAlert(result['message'] ?? 'Login failed'); @@ -353,10 +352,7 @@ class _SignInPageState extends State { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: Text( - 'OK', - style: TextStyle(color: Color(0xFF6A4C93)), - ), + child: Text('OK', style: TextStyle(color: Color(0xFF6A4C93))), ), ], ), diff --git a/frontend/lib/screens/pages/invitations_page.dart b/frontend/lib/screens/pages/invitations_page.dart index a4e6367..b1133ac 100644 --- a/frontend/lib/screens/pages/invitations_page.dart +++ b/frontend/lib/screens/pages/invitations_page.dart @@ -256,14 +256,494 @@ class _InvitationsPageState extends State { ), floatingActionButton: FloatingActionButton( onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Create invitation functionality coming soon!'), - ), + Navigator.push( + context, + MaterialPageRoute(builder: (context) => CreateInvitationPage()), ); }, backgroundColor: Color(0xFF6A4C93), - child: Icon(Icons.person_add, color: Colors.white), + child: Icon(Icons.add, color: Colors.white), + ), + ); + } +} + +class CreateInvitationPage extends StatefulWidget { + @override + _CreateInvitationPageState createState() => _CreateInvitationPageState(); +} + +class _CreateInvitationPageState extends State { + final _formKey = GlobalKey(); + final _titleController = TextEditingController(); + final _descriptionController = TextEditingController(); + final _locationController = TextEditingController(); + final _maxParticipantsController = TextEditingController(); + + DateTime? _selectedDate; + TimeOfDay? _selectedTime; + int? _selectedTagIndex; + + final List> _availableTags = [ + { + "name": "Sports", + "color_hex": "#FF6B35", + "icon_name": "sports_soccer" + }, + { + "name": "Food", + "color_hex": "#F7931E", + "icon_name": "restaurant" + }, + { + "name": "Gaming", + "color_hex": "#FFD23F", + "icon_name": "games" + }, + { + "name": "Study", + "color_hex": "#06FFA5", + "icon_name": "menu_book" + }, + { + "name": "Social", + "color_hex": "#118AB2", + "icon_name": "group" + }, + { + "name": "Travel", + "color_hex": "#06D6A0", + "icon_name": "flight" + }, + { + "name": "Music", + "color_hex": "#8E44AD", + "icon_name": "music_note" + }, + { + "name": "Movies", + "color_hex": "#E74C3C", + "icon_name": "movie" + } + ]; + + @override + void dispose() { + _titleController.dispose(); + _descriptionController.dispose(); + _locationController.dispose(); + _maxParticipantsController.dispose(); + super.dispose(); + } + + IconData _getIconFromName(String iconName) { + switch (iconName) { + case 'sports_soccer': + return Icons.sports_soccer; + case 'restaurant': + return Icons.restaurant; + case 'games': + return Icons.games; + case 'menu_book': + return Icons.menu_book; + case 'group': + return Icons.group; + case 'flight': + return Icons.flight; + case 'music_note': + return Icons.music_note; + case 'movie': + return Icons.movie; + default: + return Icons.category; + } + } + + Color _getColorFromHex(String hexColor) { + return Color(int.parse(hexColor.substring(1, 7), radix: 16) + 0xFF000000); + } + + Future _selectDate() async { + final DateTime? picked = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(Duration(days: 365)), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: Color(0xFF6A4C93), + ), + ), + child: child!, + ); + }, + ); + if (picked != null && picked != _selectedDate) { + setState(() { + _selectedDate = picked; + }); + } + } + + Future _selectTime() async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + builder: (context, child) { + return Theme( + data: Theme.of(context).copyWith( + colorScheme: ColorScheme.light( + primary: Color(0xFF6A4C93), + ), + ), + child: child!, + ); + }, + ); + if (picked != null && picked != _selectedTime) { + setState(() { + _selectedTime = picked; + }); + } + } + + void _handleSubmit() { + if (_formKey.currentState!.validate()) { + if (_selectedTagIndex == null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Please select a tag for your invitation'), + backgroundColor: Colors.red, + ), + ); + return; + } + + final invitationData = { + "title": _titleController.text.trim(), + "description": _descriptionController.text.trim(), + "date": _selectedDate?.toIso8601String(), + "time": _selectedTime != null ? "${_selectedTime!.hour}:${_selectedTime!.minute.toString().padLeft(2, '0')}" : null, + "location": _locationController.text.trim().isEmpty ? null : _locationController.text.trim(), + "max_participants": int.parse(_maxParticipantsController.text), + "tag": _availableTags[_selectedTagIndex!], + }; + + print("Invitation JSON: $invitationData"); + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Invitation created! Check console for JSON output.'), + backgroundColor: Color(0xFF6A4C93), + ), + ); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF32B0A5), Color(0xFF4600B9)], + stops: [0.0, 0.5], + ), + ), + child: SafeArea( + child: Column( + children: [ + Padding( + padding: EdgeInsets.all(16), + child: Row( + children: [ + IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: Icon(Icons.arrow_back, color: Colors.white), + ), + Text( + 'Create Invitation', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ), + ), + Expanded( + child: Container( + margin: EdgeInsets.all(16), + padding: EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 20, + offset: Offset(0, 5), + ), + ], + ), + child: Form( + key: _formKey, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Create New Invitation', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black87, + ), + textAlign: TextAlign.center, + ), + SizedBox(height: 24), + + TextFormField( + controller: _titleController, + decoration: InputDecoration( + labelText: 'Title *', + prefixIcon: Icon(Icons.title), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Color(0xFF6A4C93)), + ), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter a title'; + } + return null; + }, + ), + SizedBox(height: 16), + + TextFormField( + controller: _descriptionController, + maxLines: 3, + decoration: InputDecoration( + labelText: 'Description *', + prefixIcon: Icon(Icons.description), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Color(0xFF6A4C93)), + ), + alignLabelWithHint: true, + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter a description'; + } + return null; + }, + ), + SizedBox(height: 16), + + Row( + children: [ + Expanded( + child: InkWell( + onTap: _selectDate, + child: Container( + padding: EdgeInsets.symmetric(vertical: 16, horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[400]!), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon(Icons.calendar_today, color: Colors.grey[600]), + SizedBox(width: 12), + Text( + _selectedDate == null + ? 'Select Date (Optional)' + : '${_selectedDate!.day}/${_selectedDate!.month}/${_selectedDate!.year}', + style: TextStyle( + color: _selectedDate == null ? Colors.grey[600] : Colors.black87, + fontSize: 16, + ), + ), + ], + ), + ), + ), + ), + SizedBox(width: 12), + Expanded( + child: InkWell( + onTap: _selectTime, + child: Container( + padding: EdgeInsets.symmetric(vertical: 16, horizontal: 12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey[400]!), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Icon(Icons.access_time, color: Colors.grey[600]), + SizedBox(width: 12), + Text( + _selectedTime == null + ? 'Select Time (Optional)' + : '${_selectedTime!.hour}:${_selectedTime!.minute.toString().padLeft(2, '0')}', + style: TextStyle( + color: _selectedTime == null ? Colors.grey[600] : Colors.black87, + fontSize: 16, + ), + ), + ], + ), + ), + ), + ), + ], + ), + SizedBox(height: 16), + + TextFormField( + controller: _locationController, + decoration: InputDecoration( + labelText: 'Location (Optional)', + prefixIcon: Icon(Icons.location_on), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Color(0xFF6A4C93)), + ), + ), + ), + SizedBox(height: 16), + + TextFormField( + controller: _maxParticipantsController, + keyboardType: TextInputType.number, + decoration: InputDecoration( + labelText: 'Maximum Participants *', + prefixIcon: Icon(Icons.people), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: Color(0xFF6A4C93)), + ), + ), + validator: (value) { + if (value == null || value.trim().isEmpty) { + return 'Please enter maximum participants'; + } + final intValue = int.tryParse(value); + if (intValue == null || intValue < 1) { + return 'Please enter a valid number greater than 0'; + } + return null; + }, + ), + SizedBox(height: 20), + + Text( + 'Select Tag *', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 12), + + Wrap( + spacing: 8, + runSpacing: 8, + children: List.generate(_availableTags.length, (index) { + final tag = _availableTags[index]; + final isSelected = _selectedTagIndex == index; + return GestureDetector( + onTap: () { + setState(() { + _selectedTagIndex = index; + }); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: isSelected ? _getColorFromHex(tag['color_hex']) : Colors.grey[100], + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: isSelected ? _getColorFromHex(tag['color_hex']) : Colors.grey[300]!, + width: 2, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + _getIconFromName(tag['icon_name']), + size: 18, + color: isSelected ? Colors.white : _getColorFromHex(tag['color_hex']), + ), + SizedBox(width: 6), + Text( + tag['name'], + style: TextStyle( + color: isSelected ? Colors.white : Colors.black87, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ); + }), + ), + SizedBox(height: 32), + + Container( + height: 56, + child: ElevatedButton( + onPressed: _handleSubmit, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + foregroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + elevation: 2, + ), + child: Text( + 'Create Invitation', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), ), ); }