feat: accepting invites in the front end

This commit is contained in:
sBubshait 2025-07-22 10:34:34 +03:00
parent b65c8a7340
commit 57edd7ab14
4 changed files with 234 additions and 117 deletions

View File

@ -10,4 +10,5 @@ class ApiConstants {
// Invitation endpoints // Invitation endpoints
static const String getAllInvitationsEndpoint = '/invitations/all'; static const String getAllInvitationsEndpoint = '/invitations/all';
static const String acceptInvitationEndpoint = '/invitations/accept';
} }

View File

@ -8,10 +8,14 @@ class InvitationsPage extends StatefulWidget {
_InvitationsPageState createState() => _InvitationsPageState(); _InvitationsPageState createState() => _InvitationsPageState();
} }
class _InvitationsPageState extends State<InvitationsPage> { class _InvitationsPageState extends State<InvitationsPage>
with TickerProviderStateMixin {
InvitationsData? _invitationsData; InvitationsData? _invitationsData;
bool _isLoading = true; bool _isLoading = true;
String? _errorMessage; String? _errorMessage;
Map<String, bool> _acceptingInvitations = {};
Map<String, bool> _acceptedInvitations = {};
Map<String, AnimationController> _animationControllers = {};
@override @override
void initState() { void initState() {
@ -19,6 +23,14 @@ class _InvitationsPageState extends State<InvitationsPage> {
_loadInvitations(); _loadInvitations();
} }
@override
void dispose() {
for (final controller in _animationControllers.values) {
controller.dispose();
}
super.dispose();
}
Future<void> _loadInvitations() async { Future<void> _loadInvitations() async {
setState(() { setState(() {
_isLoading = true; _isLoading = true;
@ -44,11 +56,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(Icons.event_busy, size: 80, color: Colors.grey[400]),
Icons.event_busy,
size: 80,
color: Colors.grey[400],
),
SizedBox(height: 16), SizedBox(height: 16),
Text( Text(
'Nothing here!', 'Nothing here!',
@ -61,10 +69,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
SizedBox(height: 8), SizedBox(height: 8),
Text( Text(
'Create the first invitation now!', 'Create the first invitation now!',
style: TextStyle( style: TextStyle(fontSize: 16, color: Colors.grey[500]),
fontSize: 16,
color: Colors.grey[500],
),
), ),
], ],
), ),
@ -76,18 +81,11 @@ class _InvitationsPageState extends State<InvitationsPage> {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon( Icon(Icons.error_outline, size: 80, color: Colors.red[400]),
Icons.error_outline,
size: 80,
color: Colors.red[400],
),
SizedBox(height: 16), SizedBox(height: 16),
Text( Text(
_errorMessage ?? 'Something went wrong', _errorMessage ?? 'Something went wrong',
style: TextStyle( style: TextStyle(fontSize: 16, color: Colors.red[600]),
fontSize: 16,
color: Colors.red[600],
),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
SizedBox(height: 16), SizedBox(height: 16),
@ -104,9 +102,54 @@ class _InvitationsPageState extends State<InvitationsPage> {
); );
} }
Future<void> _acceptInvitation(Invitation invitation) async {
final invitationKey = invitation.id.toString();
setState(() {
_acceptingInvitations[invitationKey] = true;
});
final result = await InvitationsService.acceptInvitation(invitation.id);
if (mounted) {
setState(() {
_acceptingInvitations[invitationKey] = false;
});
if (result['success']) {
setState(() {
_acceptedInvitations[invitationKey] = true;
});
final controller = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);
_animationControllers[invitationKey] = controller;
controller.forward();
Future.delayed(Duration(milliseconds: 2000), () {
if (mounted) {
_loadInvitations();
}
});
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(result['message'] ?? 'Failed to accept invitation'),
backgroundColor: Colors.red,
),
);
}
}
}
Widget _buildInvitationCard(Invitation invitation, String section) { Widget _buildInvitationCard(Invitation invitation, String section) {
final invitationKey = invitation.id.toString();
bool isOwned = section == 'created'; bool isOwned = section == 'created';
bool isAccepted = section == 'accepted'; bool isAccepted = section == 'accepted';
bool isAccepting = _acceptingInvitations[invitationKey] ?? false;
bool hasBeenAccepted = _acceptedInvitations[invitationKey] ?? false;
AnimationController? animController = _animationControllers[invitationKey];
return Container( return Container(
margin: EdgeInsets.only(bottom: 16), margin: EdgeInsets.only(bottom: 16),
@ -121,10 +164,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
offset: Offset(0, 2), offset: Offset(0, 2),
), ),
], ],
border: Border.all( border: Border.all(color: Colors.grey.withOpacity(0.2), width: 1),
color: Colors.grey.withOpacity(0.2),
width: 1,
),
), ),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -134,12 +174,16 @@ class _InvitationsPageState extends State<InvitationsPage> {
Container( Container(
padding: EdgeInsets.all(8), padding: EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: InvitationUtils.getColorFromHex(invitation.tag.colorHex).withOpacity(0.1), color: InvitationUtils.getColorFromHex(
invitation.tag.colorHex,
).withOpacity(0.1),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: Icon( child: Icon(
InvitationUtils.getIconFromName(invitation.tag.iconName), InvitationUtils.getIconFromName(invitation.tag.iconName),
color: InvitationUtils.getColorFromHex(invitation.tag.colorHex), color: InvitationUtils.getColorFromHex(
invitation.tag.colorHex,
),
size: 24, size: 24,
), ),
), ),
@ -158,10 +202,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
), ),
Text( Text(
'by ${invitation.creator.displayName}', 'by ${invitation.creator.displayName}',
style: TextStyle( style: TextStyle(fontSize: 12, color: Colors.grey[600]),
fontSize: 12,
color: Colors.grey[600],
),
), ),
], ],
), ),
@ -169,7 +210,9 @@ class _InvitationsPageState extends State<InvitationsPage> {
Container( Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: InvitationUtils.getColorFromHex(invitation.tag.colorHex).withOpacity(0.1), color: InvitationUtils.getColorFromHex(
invitation.tag.colorHex,
).withOpacity(0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: Text(
@ -177,13 +220,16 @@ class _InvitationsPageState extends State<InvitationsPage> {
style: TextStyle( style: TextStyle(
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: InvitationUtils.getColorFromHex(invitation.tag.colorHex), color: InvitationUtils.getColorFromHex(
invitation.tag.colorHex,
),
), ),
), ),
), ),
], ],
), ),
if (invitation.description != null && invitation.description!.isNotEmpty) ...[ if (invitation.description != null &&
invitation.description!.isNotEmpty) ...[
SizedBox(height: 12), SizedBox(height: 12),
Text( Text(
InvitationUtils.truncateDescription(invitation.description), InvitationUtils.truncateDescription(invitation.description),
@ -202,10 +248,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
SizedBox(width: 4), SizedBox(width: 4),
Text( Text(
invitation.location!, invitation.location!,
style: TextStyle( style: TextStyle(fontSize: 13, color: Colors.grey[600]),
fontSize: 13,
color: Colors.grey[600],
),
), ),
], ],
), ),
@ -218,10 +261,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
SizedBox(width: 4), SizedBox(width: 4),
Text( Text(
InvitationUtils.getRelativeDateTime(invitation.dateTime!), InvitationUtils.getRelativeDateTime(invitation.dateTime!),
style: TextStyle( style: TextStyle(fontSize: 13, color: Colors.grey[600]),
fontSize: 13,
color: Colors.grey[600],
),
), ),
], ],
), ),
@ -234,21 +274,21 @@ class _InvitationsPageState extends State<InvitationsPage> {
decoration: BoxDecoration( decoration: BoxDecoration(
color: InvitationUtils.getParticipantsStatusColor( color: InvitationUtils.getParticipantsStatusColor(
invitation.currentAttendees, invitation.currentAttendees,
invitation.maxParticipants invitation.maxParticipants,
).withOpacity(0.1), ).withOpacity(0.1),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
child: Text( child: Text(
InvitationUtils.getParticipantsStatus( InvitationUtils.getParticipantsStatus(
invitation.currentAttendees, invitation.currentAttendees,
invitation.maxParticipants invitation.maxParticipants,
), ),
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: InvitationUtils.getParticipantsStatusColor( color: InvitationUtils.getParticipantsStatusColor(
invitation.currentAttendees, invitation.currentAttendees,
invitation.maxParticipants invitation.maxParticipants,
), ),
), ),
), ),
@ -265,21 +305,58 @@ class _InvitationsPageState extends State<InvitationsPage> {
width: double.infinity, width: double.infinity,
height: 44, height: 44,
child: ElevatedButton( child: ElevatedButton(
onPressed: isOwned || isAccepted ? null : () { onPressed: isOwned || isAccepted || isAccepting || hasBeenAccepted
// TODO: Implement accept invitation ? null
: () {
_acceptInvitation(invitation);
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: isAccepted backgroundColor: (isAccepted || hasBeenAccepted)
? Colors.green ? Colors.green
: (isOwned ? Colors.grey[400] : Color(0xFF6A4C93)), : (isOwned ? Colors.grey[400] : Color(0xFF6A4C93)),
foregroundColor: Colors.white, foregroundColor: Colors.white,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
elevation: isOwned || isAccepted ? 0 : 2, elevation: isOwned || isAccepted || hasBeenAccepted ? 0 : 2,
), ),
child: isAccepting
? SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: hasBeenAccepted && animController != null
? AnimatedBuilder(
animation: animController,
builder: (context, child) {
if (animController.value < 0.5) {
return Text(
'Accepted ✓',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
);
} else {
return Opacity(
opacity: 1.0 - ((animController.value - 0.5) * 2),
child: Text( child: Text(
isAccepted 'Accepted ✓',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
);
}
},
)
: Text(
(isAccepted || hasBeenAccepted)
? 'Accepted ✓' ? 'Accepted ✓'
: (isOwned ? 'View/Edit' : 'Accept Invite'), : (isOwned ? 'View/Edit' : 'Accept Invite'),
style: TextStyle( style: TextStyle(
@ -294,7 +371,11 @@ class _InvitationsPageState extends State<InvitationsPage> {
); );
} }
Widget _buildInvitationSection(String title, List<Invitation> invitations, String section) { Widget _buildInvitationSection(
String title,
List<Invitation> invitations,
String section,
) {
if (invitations.isEmpty) return SizedBox.shrink(); if (invitations.isEmpty) return SizedBox.shrink();
return Column( return Column(
@ -311,10 +392,14 @@ class _InvitationsPageState extends State<InvitationsPage> {
), ),
), ),
), ),
...invitations.map((invitation) => Padding( ...invitations
.map(
(invitation) => Padding(
padding: EdgeInsets.symmetric(horizontal: 16), padding: EdgeInsets.symmetric(horizontal: 16),
child: _buildInvitationCard(invitation, section), child: _buildInvitationCard(invitation, section),
)).toList(), ),
)
.toList(),
], ],
); );
} }
@ -336,10 +421,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
), ),
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
actions: [ actions: [
IconButton( IconButton(icon: Icon(Icons.refresh), onPressed: _loadInvitations),
icon: Icon(Icons.refresh),
onPressed: _loadInvitations,
),
], ],
), ),
body: _isLoading body: _isLoading

View File

@ -6,21 +6,21 @@ import 'http_service.dart';
class InvitationsService { class InvitationsService {
static Future<Map<String, dynamic>> getAllInvitations() async { static Future<Map<String, dynamic>> getAllInvitations() async {
try { try {
final response = await HttpService.get(ApiConstants.getAllInvitationsEndpoint); final response = await HttpService.get(
ApiConstants.getAllInvitationsEndpoint,
);
if (response.statusCode == 200) { if (response.statusCode == 200) {
final responseData = jsonDecode(response.body); final responseData = jsonDecode(response.body);
final invitationsResponse = InvitationsResponse.fromJson(responseData); final invitationsResponse = InvitationsResponse.fromJson(responseData);
if (invitationsResponse.status) { if (invitationsResponse.status) {
return { return {'success': true, 'data': invitationsResponse.data};
'success': true,
'data': invitationsResponse.data,
};
} else { } else {
return { return {
'success': false, 'success': false,
'message': invitationsResponse.message ?? 'Failed to fetch invitations', 'message':
invitationsResponse.message ?? 'Failed to fetch invitations',
}; };
} }
} else if (response.statusCode == 401) { } else if (response.statusCode == 401) {
@ -47,4 +47,42 @@ class InvitationsService {
}; };
} }
} }
static Future<Map<String, dynamic>> acceptInvitation(int invitationId) async {
try {
final response = await HttpService.post(
ApiConstants.acceptInvitationEndpoint,
{'id': invitationId},
);
if (response.statusCode == 200) {
final responseData = jsonDecode(response.body);
return {
'success': responseData['status'] ?? false,
'message': responseData['message'] ?? 'Request completed',
};
} else if (response.statusCode == 401) {
return {
'success': false,
'message': 'Session expired. Please login again.',
};
} else if (response.statusCode == 403) {
return {
'success': false,
'message': 'Access denied. Invalid credentials.',
};
} else {
return {
'success': false,
'message': 'Server error (${response.statusCode})',
};
}
} catch (e) {
print('Error accepting invitation: $e');
return {
'success': false,
'message': 'Network error. Please check your connection.',
};
}
}
} }

View File

@ -71,7 +71,8 @@ class InvitationUtils {
return 'today at $timeString'; return 'today at $timeString';
} else if (eventDate == tomorrow) { } else if (eventDate == tomorrow) {
return 'tomorrow at $timeString'; return 'tomorrow at $timeString';
} else if (eventDate.isAfter(today) && eventDate.isBefore(today.add(Duration(days: 7)))) { } else if (eventDate.isAfter(today) &&
eventDate.isBefore(today.add(Duration(days: 7)))) {
List<String> weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; List<String> weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
return '${weekdays[eventDate.weekday - 1]} at $timeString'; return '${weekdays[eventDate.weekday - 1]} at $timeString';
} else { } else {
@ -94,13 +95,8 @@ class InvitationUtils {
} }
static String getParticipantsStatus(int current, int max) { static String getParticipantsStatus(int current, int max) {
int needed = max - current;
if (needed <= 2 && needed > 0) {
return '$needed more person${needed == 1 ? '' : 's'} needed';
} else {
return '$current/$max'; return '$current/$max';
} }
}
static Color getParticipantsStatusColor(int current, int max) { static Color getParticipantsStatusColor(int current, int max) {
int needed = max - current; int needed = max - current;