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

View File

@ -70,11 +70,8 @@ class _CreatePostScreenState extends State<CreatePostScreen> {
final remainingChars = _maxCharacters - _bodyController.text.length;
final isOverLimit = remainingChars < 0;
return WillPopScope(
onWillPop: () async {
// Allow back navigation to go back to feed page only
return true;
},
return PopScope(
canPop: true, // Allow back navigation to go back to feed page only
child: Scaffold(
appBar: AppBar(
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/invitations_page.dart';
import 'pages/profile_page.dart';
import '../services/invitations_service.dart';
class HomeScreen extends StatefulWidget {
@override
@ -10,9 +11,70 @@ class HomeScreen extends StatefulWidget {
class _HomeScreenState extends State<HomeScreen> {
int _currentIndex = 0;
int _availableInvitationsCount = 0;
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
Widget build(BuildContext context) {
return PopScope(
@ -32,7 +94,7 @@ class _HomeScreenState extends State<HomeScreen> {
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Feed'),
BottomNavigationBarItem(
icon: Icon(Icons.mail),
icon: _buildInvitationsBadge(),
label: 'Invitations',
),
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),

View File

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

View File

@ -14,7 +14,15 @@ class _FeedPageState extends State<FeedPage> {
@override
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(
title: Text('Feed', style: TextStyle(fontWeight: FontWeight.w600)),
backgroundColor: Colors.white,
@ -45,6 +53,7 @@ class _FeedPageState extends State<FeedPage> {
backgroundColor: Color(0xFF6A4C93),
child: Icon(Icons.edit, color: Colors.white),
),
),
);
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import '../../models/invitation_models.dart';
import '../../services/invitations_service.dart';
import '../../services/user_service.dart';
import '../../utils/invitation_utils.dart';
class InvitationDetailsPage extends StatefulWidget {
@ -23,11 +24,14 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
InvitationDetails? _invitationDetails;
bool _isLoading = true;
bool _isCancelling = false;
bool _isAccepting = false;
bool _isCurrentlyParticipant = false;
String? _errorMessage;
@override
void initState() {
super.initState();
_isCurrentlyParticipant = widget.isParticipant;
_loadInvitationDetails();
}
@ -48,6 +52,11 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
_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
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
// Allow back navigation to go back to invitations page only
return true;
},
return PopScope(
canPop: true, // Allow back navigation to go back to invitations page only
child: Scaffold(
backgroundColor: Colors.grey[50],
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),
Container(
width: double.infinity,

View File

@ -49,7 +49,9 @@ class _InvitationsPageState extends State<InvitationsPage>
_isLoading = false;
_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) {
@ -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) {
setState(() {
@ -528,70 +532,82 @@ class _InvitationsPageState extends State<InvitationsPage>
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
'Invitations',
style: TextStyle(fontWeight: FontWeight.w600),
return PopScope(
canPop: false, // Prevent back navigation from invitations page
onPopInvoked: (bool didPop) {
// Prevent any pop behavior, including iOS back gesture
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,
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)),
],
),
body: _isLoading
? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93)))
: _errorMessage != null
? _buildErrorState()
: _invitationsData?.isEmpty ?? true
? _buildEmptyState()
: RefreshIndicator(
onRefresh: () => _loadInvitations(forceRefresh: true),
color: Color(0xFF6A4C93),
child: SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Column(
children: [
SizedBox(height: 16),
_buildInvitationSection(
'My Invitations',
_invitationsData?.created ?? [],
'created',
),
_buildInvitationSection(
'Accepted Invitations',
_invitationsData?.accepted ?? [],
'accepted',
),
_buildInvitationSection(
'Available Invitations',
_invitationsData?.available ?? [],
'available',
),
SizedBox(height: 80),
],
body: _isLoading
? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93)))
: _errorMessage != null
? _buildErrorState()
: _invitationsData?.isEmpty ?? true
? _buildEmptyState()
: RefreshIndicator(
onRefresh: () => _loadInvitations(forceRefresh: true),
color: Color(0xFF6A4C93),
child: SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Column(
children: [
SizedBox(height: 16),
_buildInvitationSection(
'My Invitations',
_invitationsData?.created ?? [],
'created',
),
_buildInvitationSection(
'Accepted Invitations',
_invitationsData?.accepted ?? [],
'accepted',
),
_buildInvitationSection(
'Available Invitations',
_invitationsData?.available ?? [],
'available',
),
SizedBox(height: 80),
],
),
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => CreateInvitationPage()),
);
if (result == true) {
_loadInvitations(forceRefresh: true);
}
},
backgroundColor: Color(0xFF6A4C93),
child: Icon(Icons.add, color: Colors.white),
floatingActionButton: FloatingActionButton(
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => CreateInvitationPage()),
);
if (result == true) {
_loadInvitations(forceRefresh: true);
}
},
backgroundColor: Color(0xFF6A4C93),
child: Icon(Icons.add, color: Colors.white),
),
),
);
}
@ -734,373 +750,370 @@ class _CreateInvitationPageState extends State<CreateInvitationPage> {
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
// Allow back navigation to go back to invitations page only
return true;
},
return PopScope(
canPop: true, // Allow back navigation to go back to invitations page only
child: Scaffold(
body: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF32B0A5), Color(0xFF4600B9)],
stops: [0.0, 0.5],
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,
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),
),
),
],
),
),
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),
Text(
'Create Invitation',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
],
),
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),
),
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,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Color(0xFF6A4C93),
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),
),
),
),
),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter a title';
}
return null;
},
),
SizedBox(height: 16),
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),
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),
),
),
),
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) {
if (value == null || value.trim().isEmpty) {
return 'Please enter a description';
}
return null;
},
),
SizedBox(height: 16),
SizedBox(height: 20),
Row(
children: [
Expanded(
child: InkWell(
onTap: _selectDate,
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(
vertical: 16,
horizontal: 12,
vertical: 8,
),
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
? InvitationUtils.getColorFromHex(
tag['color_hex'],
)
: Colors.grey[300]!,
width: 2,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
InvitationUtils.getIconFromName(
tag['icon_name'],
),
size: 18,
: Colors.grey[100],
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected
? Colors.white
: InvitationUtils.getColorFromHex(
? InvitationUtils.getColorFromHex(
tag['color_hex'],
),
)
: Colors.grey[300]!,
width: 2,
),
SizedBox(width: 6),
Text(
tag['name'],
style: TextStyle(
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
InvitationUtils.getIconFromName(
tag['icon_name'],
),
size: 18,
color: isSelected
? Colors.white
: Colors.black87,
fontWeight: FontWeight.w500,
: InvitationUtils.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: _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
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(
title: Text('Profile', style: TextStyle(fontWeight: FontWeight.w600)),
backgroundColor: Colors.white,
@ -318,6 +326,7 @@ class _ProfilePageState extends State<ProfilePage> {
],
],
),
),
),
);
}
@ -674,11 +683,8 @@ class _SettingsPageState extends State<SettingsPage> {
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () async {
// Allow back navigation to go back to profile page only
return true;
},
return PopScope(
canPop: true, // Allow back navigation to go back to profile page only
child: Scaffold(
appBar: AppBar(
title: Text('Settings', style: TextStyle(fontWeight: FontWeight.w600)),

View File

@ -32,6 +32,21 @@
<title>Wesal</title>
<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>
<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>
</body>