Merge pull request #11 from sBubshait/feature/ux

Feature/ux
This commit is contained in:
Saleh Bubshait 2025-07-28 11:35:15 +03:00 committed by GitHub
commit 11b577bbd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 624 additions and 424 deletions

View File

@ -129,11 +129,8 @@ class _LandingPageState extends State<LandingPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return PopScope(
onWillPop: () async { canPop: false, // Prevent back navigation from landing page
// Prevent back navigation from landing page
return false;
},
child: Scaffold( child: Scaffold(
body: Stack( body: Stack(
children: [ children: [
@ -379,11 +376,8 @@ class _SignInPageState extends State<SignInPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return PopScope(
onWillPop: () async { canPop: false, // Prevent back navigation from sign in page
// Prevent back navigation from sign in page
return false;
},
child: Scaffold( child: Scaffold(
body: Container( body: Container(
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@ -70,11 +70,8 @@ class _CreatePostScreenState extends State<CreatePostScreen> {
final remainingChars = _maxCharacters - _bodyController.text.length; final remainingChars = _maxCharacters - _bodyController.text.length;
final isOverLimit = remainingChars < 0; final isOverLimit = remainingChars < 0;
return WillPopScope( return PopScope(
onWillPop: () async { canPop: true, // Allow back navigation to go back to feed page only
// Allow back navigation to go back to feed page only
return true;
},
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Create Post', style: TextStyle(fontWeight: FontWeight.w600)), title: Text('Create Post', style: TextStyle(fontWeight: FontWeight.w600)),

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'pages/feed_page.dart'; import 'pages/feed_page.dart';
import 'pages/invitations_page.dart'; import 'pages/invitations_page.dart';
import 'pages/profile_page.dart'; import 'pages/profile_page.dart';
import '../services/invitations_service.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
@override @override
@ -10,9 +11,70 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> { class _HomeScreenState extends State<HomeScreen> {
int _currentIndex = 0; int _currentIndex = 0;
int _availableInvitationsCount = 0;
final List<Widget> _pages = [FeedPage(), InvitationsPage(), ProfilePage()]; final List<Widget> _pages = [FeedPage(), InvitationsPage(), ProfilePage()];
@override
void initState() {
super.initState();
// Start polling and listen to invitations stream
InvitationsService.startPolling();
_listenToInvitations();
}
@override
void dispose() {
InvitationsService.stopPolling();
super.dispose();
}
void _listenToInvitations() {
InvitationsService.getInvitationsStream().listen((invitationsData) {
if (mounted) {
setState(() {
_availableInvitationsCount = invitationsData.available.length;
});
}
});
}
Widget _buildInvitationsBadge() {
if (_availableInvitationsCount == 0) {
return Icon(Icons.mail);
}
return Stack(
children: [
Icon(Icons.mail),
Positioned(
right: 0,
top: 0,
child: Container(
padding: EdgeInsets.all(1),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
constraints: BoxConstraints(
minWidth: 16,
minHeight: 16,
),
child: Text(
_availableInvitationsCount > 99 ? '99+' : '$_availableInvitationsCount',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PopScope( return PopScope(
@ -32,7 +94,7 @@ class _HomeScreenState extends State<HomeScreen> {
items: [ items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Feed'), BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Feed'),
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.mail), icon: _buildInvitationsBadge(),
label: 'Invitations', label: 'Invitations',
), ),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'), BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),

View File

@ -79,11 +79,8 @@ class _NotificationPermissionScreenState
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return PopScope(
onWillPop: () async { canPop: false, // Prevent back navigation from notification permission screen
// Prevent back navigation from notification permission screen
return false;
},
child: Scaffold( child: Scaffold(
body: Container( body: Container(
decoration: BoxDecoration( decoration: BoxDecoration(

View File

@ -14,7 +14,15 @@ class _FeedPageState extends State<FeedPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return PopScope(
canPop: false, // Prevent back navigation from feed page
onPopInvoked: (bool didPop) {
// Prevent any pop behavior, including iOS back gesture
if (!didPop) {
// Do nothing - stay on feed page
}
},
child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Feed', style: TextStyle(fontWeight: FontWeight.w600)), title: Text('Feed', style: TextStyle(fontWeight: FontWeight.w600)),
backgroundColor: Colors.white, backgroundColor: Colors.white,
@ -45,6 +53,7 @@ class _FeedPageState extends State<FeedPage> {
backgroundColor: Color(0xFF6A4C93), backgroundColor: Color(0xFF6A4C93),
child: Icon(Icons.edit, color: Colors.white), child: Icon(Icons.edit, color: Colors.white),
), ),
),
); );
} }

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../models/invitation_models.dart'; import '../../models/invitation_models.dart';
import '../../services/invitations_service.dart'; import '../../services/invitations_service.dart';
import '../../services/user_service.dart';
import '../../utils/invitation_utils.dart'; import '../../utils/invitation_utils.dart';
class InvitationDetailsPage extends StatefulWidget { class InvitationDetailsPage extends StatefulWidget {
@ -23,11 +24,14 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
InvitationDetails? _invitationDetails; InvitationDetails? _invitationDetails;
bool _isLoading = true; bool _isLoading = true;
bool _isCancelling = false; bool _isCancelling = false;
bool _isAccepting = false;
bool _isCurrentlyParticipant = false;
String? _errorMessage; String? _errorMessage;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_isCurrentlyParticipant = widget.isParticipant;
_loadInvitationDetails(); _loadInvitationDetails();
} }
@ -48,6 +52,11 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
_errorMessage = result['message']; _errorMessage = result['message'];
} }
}); });
// Update participation status after loading details
if (result['success']) {
await _updateParticipationStatus();
}
} }
} }
@ -150,13 +159,56 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
} }
} }
Future<void> _updateParticipationStatus() async {
if (_invitationDetails == null) return;
final userResult = await UserService.getCurrentUser();
if (userResult['success'] && userResult['data'] != null) {
final currentUserId = userResult['data']['id'];
final isParticipant = _invitationDetails!.attendees.any((attendee) => attendee.id == currentUserId);
setState(() {
_isCurrentlyParticipant = isParticipant;
});
}
}
Future<void> _acceptInvitation() async {
setState(() {
_isAccepting = true;
});
final result = await InvitationsService.acceptInvitation(widget.invitationId);
if (mounted) {
setState(() {
_isAccepting = false;
});
if (result['success']) {
// Reload invitation details to reflect the new state
await _loadInvitationDetails();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Invitation accepted successfully!'),
backgroundColor: Colors.green,
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result['message'] ?? 'Failed to accept invitation'),
backgroundColor: Colors.red,
),
);
}
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return PopScope(
onWillPop: () async { canPop: true, // Allow back navigation to go back to invitations page only
// Allow back navigation to go back to invitations page only
return true;
},
child: Scaffold( child: Scaffold(
backgroundColor: Colors.grey[50], backgroundColor: Colors.grey[50],
appBar: AppBar( appBar: AppBar(
@ -512,7 +564,45 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
), ),
), ),
], ],
if (widget.isParticipant) ...[
// Accept button for non-participants who are not owners
if (!_isCurrentlyParticipant && !widget.isOwner) ...[
SizedBox(height: 32),
Container(
width: double.infinity,
height: 56,
child: ElevatedButton(
onPressed: _isAccepting ? null : _acceptInvitation,
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: _isAccepting
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Text(
'Accept Invitation',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
// Cancel/Leave button for participants
if (_isCurrentlyParticipant) ...[
SizedBox(height: 32), SizedBox(height: 32),
Container( Container(
width: double.infinity, width: double.infinity,

View File

@ -49,7 +49,9 @@ class _InvitationsPageState extends State<InvitationsPage>
_isLoading = false; _isLoading = false;
_errorMessage = null; _errorMessage = null;
}); });
print('Invitations UI updated with ${updatedInvitationsData.created.length + updatedInvitationsData.accepted.length + updatedInvitationsData.available.length} total invitations'); print(
'Invitations UI updated with ${updatedInvitationsData.created.length + updatedInvitationsData.accepted.length + updatedInvitationsData.available.length} total invitations',
);
} }
}, },
onError: (error) { onError: (error) {
@ -69,7 +71,9 @@ class _InvitationsPageState extends State<InvitationsPage>
}); });
} }
final result = await InvitationsService.getAllInvitations(forceRefresh: forceRefresh); final result = await InvitationsService.getAllInvitations(
forceRefresh: forceRefresh,
);
if (mounted) { if (mounted) {
setState(() { setState(() {
@ -528,70 +532,82 @@ class _InvitationsPageState extends State<InvitationsPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return PopScope(
appBar: AppBar( canPop: false, // Prevent back navigation from invitations page
title: Text( onPopInvoked: (bool didPop) {
'Invitations', // Prevent any pop behavior, including iOS back gesture
style: TextStyle(fontWeight: FontWeight.w600), if (!didPop) {
// Do nothing - stay on invitations page
}
},
child: Scaffold(
appBar: AppBar(
title: Text(
'Invitations',
style: TextStyle(fontWeight: FontWeight.w600),
),
backgroundColor: Colors.white,
foregroundColor: Color(0xFF6A4C93),
elevation: 0,
bottom: PreferredSize(
preferredSize: Size.fromHeight(1),
child: Container(height: 1, color: Colors.grey[200]),
),
automaticallyImplyLeading: false,
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => _loadInvitations(forceRefresh: true),
),
],
), ),
backgroundColor: Colors.white, body: _isLoading
foregroundColor: Color(0xFF6A4C93), ? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93)))
elevation: 0, : _errorMessage != null
bottom: PreferredSize( ? _buildErrorState()
preferredSize: Size.fromHeight(1), : _invitationsData?.isEmpty ?? true
child: Container(height: 1, color: Colors.grey[200]), ? _buildEmptyState()
), : RefreshIndicator(
automaticallyImplyLeading: false, onRefresh: () => _loadInvitations(forceRefresh: true),
actions: [ color: Color(0xFF6A4C93),
IconButton(icon: Icon(Icons.refresh), onPressed: () => _loadInvitations(forceRefresh: true)), child: SingleChildScrollView(
], physics: AlwaysScrollableScrollPhysics(),
), child: Column(
body: _isLoading children: [
? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93))) SizedBox(height: 16),
: _errorMessage != null _buildInvitationSection(
? _buildErrorState() 'My Invitations',
: _invitationsData?.isEmpty ?? true _invitationsData?.created ?? [],
? _buildEmptyState() 'created',
: RefreshIndicator( ),
onRefresh: () => _loadInvitations(forceRefresh: true), _buildInvitationSection(
color: Color(0xFF6A4C93), 'Accepted Invitations',
child: SingleChildScrollView( _invitationsData?.accepted ?? [],
physics: AlwaysScrollableScrollPhysics(), 'accepted',
child: Column( ),
children: [ _buildInvitationSection(
SizedBox(height: 16), 'Available Invitations',
_buildInvitationSection( _invitationsData?.available ?? [],
'My Invitations', 'available',
_invitationsData?.created ?? [], ),
'created', SizedBox(height: 80),
), ],
_buildInvitationSection( ),
'Accepted Invitations',
_invitationsData?.accepted ?? [],
'accepted',
),
_buildInvitationSection(
'Available Invitations',
_invitationsData?.available ?? [],
'available',
),
SizedBox(height: 80),
],
), ),
), ),
), floatingActionButton: FloatingActionButton(
floatingActionButton: FloatingActionButton( onPressed: () async {
onPressed: () async { final result = await Navigator.push(
final result = await Navigator.push( context,
context, MaterialPageRoute(builder: (context) => CreateInvitationPage()),
MaterialPageRoute(builder: (context) => CreateInvitationPage()), );
); if (result == true) {
if (result == true) { _loadInvitations(forceRefresh: true);
_loadInvitations(forceRefresh: true); }
} },
}, backgroundColor: Color(0xFF6A4C93),
backgroundColor: Color(0xFF6A4C93), child: Icon(Icons.add, color: Colors.white),
child: Icon(Icons.add, color: Colors.white), ),
), ),
); );
} }
@ -734,373 +750,370 @@ class _CreateInvitationPageState extends State<CreateInvitationPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return PopScope(
onWillPop: () async { canPop: true, // Allow back navigation to go back to invitations page only
// Allow back navigation to go back to invitations page only
return true;
},
child: Scaffold( child: Scaffold(
body: Container( body: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [Color(0xFF32B0A5), Color(0xFF4600B9)], colors: [Color(0xFF32B0A5), Color(0xFF4600B9)],
stops: [0.0, 0.5], stops: [0.0, 0.5],
),
), ),
), child: SafeArea(
child: SafeArea( child: Column(
child: Column( children: [
children: [ Padding(
Padding( padding: EdgeInsets.all(16),
padding: EdgeInsets.all(16), child: Row(
child: Row( children: [
children: [ IconButton(
IconButton( onPressed: () => Navigator.of(context).pop(),
onPressed: () => Navigator.of(context).pop(), icon: Icon(Icons.arrow_back, color: Colors.white),
icon: Icon(Icons.arrow_back, color: Colors.white),
),
Text(
'Create Invitation',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
), ),
), Text(
], 'Create Invitation',
), style: TextStyle(
), fontSize: 20,
Expanded( fontWeight: FontWeight.w600,
child: Container( color: Colors.white,
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, Expanded(
child: SingleChildScrollView( child: Container(
child: Column( margin: EdgeInsets.all(16),
crossAxisAlignment: CrossAxisAlignment.stretch, padding: EdgeInsets.all(24),
children: [ decoration: BoxDecoration(
Text( color: Colors.white,
'Create New Invitation', borderRadius: BorderRadius.circular(16),
style: TextStyle( boxShadow: [
fontSize: 24, BoxShadow(
fontWeight: FontWeight.bold, color: Colors.black.withOpacity(0.1),
color: Colors.black87, blurRadius: 20,
), offset: Offset(0, 5),
textAlign: TextAlign.center, ),
), ],
SizedBox(height: 24), ),
child: Form(
TextFormField( key: _formKey,
controller: _titleController, child: SingleChildScrollView(
decoration: InputDecoration( child: Column(
labelText: 'Title *', crossAxisAlignment: CrossAxisAlignment.stretch,
prefixIcon: Icon(Icons.title), children: [
border: OutlineInputBorder( Text(
borderRadius: BorderRadius.circular(12), 'Create New Invitation',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
), ),
focusedBorder: OutlineInputBorder( textAlign: TextAlign.center,
borderRadius: BorderRadius.circular(12), ),
borderSide: BorderSide( SizedBox(height: 24),
color: Color(0xFF6A4C93),
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),
),
), ),
), ),
), ),
validator: (value) { SizedBox(height: 16),
if (value == null || value.trim().isEmpty) {
return 'Please enter a title';
}
return null;
},
),
SizedBox(height: 16),
TextFormField( TextFormField(
controller: _descriptionController, controller: _maxParticipantsController,
maxLines: 3, keyboardType: TextInputType.number,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Description *', labelText: 'Maximum Participants *',
prefixIcon: Icon(Icons.description), prefixIcon: Icon(Icons.people),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
borderSide: BorderSide( borderSide: BorderSide(
color: Color(0xFF6A4C93), color: Color(0xFF6A4C93),
),
), ),
), ),
alignLabelWithHint: true, 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;
},
), ),
validator: (value) { SizedBox(height: 20),
if (value == null || value.trim().isEmpty) {
return 'Please enter a description';
}
return null;
},
),
SizedBox(height: 16),
Row( Text(
children: [ 'Select Tag *',
Expanded( style: TextStyle(
child: InkWell( fontSize: 16,
onTap: _selectDate, 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( child: Container(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
vertical: 16,
horizontal: 12, horizontal: 12,
vertical: 8,
), ),
decoration: BoxDecoration( 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
? InvitationUtils.getColorFromHex(
tag['color_hex'],
)
: Colors.grey[100],
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected color: isSelected
? InvitationUtils.getColorFromHex( ? InvitationUtils.getColorFromHex(
tag['color_hex'], tag['color_hex'],
) )
: Colors.grey[300]!, : Colors.grey[100],
width: 2, borderRadius: BorderRadius.circular(20),
), border: Border.all(
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
InvitationUtils.getIconFromName(
tag['icon_name'],
),
size: 18,
color: isSelected color: isSelected
? Colors.white ? InvitationUtils.getColorFromHex(
: InvitationUtils.getColorFromHex(
tag['color_hex'], tag['color_hex'],
), )
: Colors.grey[300]!,
width: 2,
), ),
SizedBox(width: 6), ),
Text( child: Row(
tag['name'], mainAxisSize: MainAxisSize.min,
style: TextStyle( children: [
Icon(
InvitationUtils.getIconFromName(
tag['icon_name'],
),
size: 18,
color: isSelected color: isSelected
? Colors.white ? Colors.white
: Colors.black87, : InvitationUtils.getColorFromHex(
fontWeight: FontWeight.w500, 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: _isSubmitting ? null : _handleSubmit,
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
),
child: _isSubmitting
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: Text(
'Create Invitation',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
), ),
), ),
],
),
),
);
}),
),
SizedBox(height: 32),
Container(
height: 56,
child: ElevatedButton(
onPressed: _isSubmitting ? null : _handleSubmit,
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 2,
), ),
child: _isSubmitting
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
)
: Text(
'Create Invitation',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
), ),
), ],
], ),
), ),
), ),
), ),
), ),
), ],
], ),
), ),
), ),
),
), ),
); );
} }

View File

@ -170,7 +170,15 @@ class _ProfilePageState extends State<ProfilePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return PopScope(
canPop: false, // Prevent back navigation from profile page
onPopInvoked: (bool didPop) {
// Prevent any pop behavior, including iOS back gesture
if (!didPop) {
// Do nothing - stay on profile page
}
},
child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Profile', style: TextStyle(fontWeight: FontWeight.w600)), title: Text('Profile', style: TextStyle(fontWeight: FontWeight.w600)),
backgroundColor: Colors.white, backgroundColor: Colors.white,
@ -318,6 +326,7 @@ class _ProfilePageState extends State<ProfilePage> {
], ],
], ],
), ),
),
), ),
); );
} }
@ -674,11 +683,8 @@ class _SettingsPageState extends State<SettingsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return WillPopScope( return PopScope(
onWillPop: () async { canPop: true, // Allow back navigation to go back to profile page only
// Allow back navigation to go back to profile page only
return true;
},
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Settings', style: TextStyle(fontWeight: FontWeight.w600)), title: Text('Settings', style: TextStyle(fontWeight: FontWeight.w600)),

View File

@ -32,6 +32,21 @@
<title>Wesal</title> <title>Wesal</title>
<link rel="manifest" href="manifest.json"> <link rel="manifest" href="manifest.json">
<style>
body {
overscroll-behavior-x: none;
-webkit-overflow-scrolling: touch;
}
/* Prevent pull-to-refresh and back gesture */
html,
body {
overflow-x: hidden;
position: fixed;
width: 100%;
height: 100%;
}
</style>
</head> </head>
<body> <body>
@ -50,6 +65,23 @@
}); });
}); });
} }
// Prevent iOS Safari back gesture
window.addEventListener('touchstart', function (e) {
if (e.touches.length > 1) return;
const touch = e.touches[0];
if (touch.clientX < 20) { // Left edge
e.preventDefault();
}
}, { passive: false });
// Override browser back
window.addEventListener('popstate', function (e) {
e.preventDefault();
// Send message to Flutter
window.dispatchEvent(new CustomEvent('browser-back'));
});
</script> </script>
</body> </body>