feat: accepting invites in the front end
This commit is contained in:
parent
b65c8a7340
commit
57edd7ab14
@ -10,4 +10,5 @@ class ApiConstants {
|
||||
|
||||
// Invitation endpoints
|
||||
static const String getAllInvitationsEndpoint = '/invitations/all';
|
||||
static const String acceptInvitationEndpoint = '/invitations/accept';
|
||||
}
|
||||
|
||||
@ -8,10 +8,14 @@ class InvitationsPage extends StatefulWidget {
|
||||
_InvitationsPageState createState() => _InvitationsPageState();
|
||||
}
|
||||
|
||||
class _InvitationsPageState extends State<InvitationsPage> {
|
||||
class _InvitationsPageState extends State<InvitationsPage>
|
||||
with TickerProviderStateMixin {
|
||||
InvitationsData? _invitationsData;
|
||||
bool _isLoading = true;
|
||||
String? _errorMessage;
|
||||
Map<String, bool> _acceptingInvitations = {};
|
||||
Map<String, bool> _acceptedInvitations = {};
|
||||
Map<String, AnimationController> _animationControllers = {};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -19,6 +23,14 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
_loadInvitations();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (final controller in _animationControllers.values) {
|
||||
controller.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadInvitations() async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
@ -44,11 +56,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.event_busy,
|
||||
size: 80,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
Icon(Icons.event_busy, size: 80, color: Colors.grey[400]),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Nothing here!',
|
||||
@ -61,10 +69,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Create the first invitation now!',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[500]),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -76,18 +81,11 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 80,
|
||||
color: Colors.red[400],
|
||||
),
|
||||
Icon(Icons.error_outline, size: 80, color: Colors.red[400]),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
_errorMessage ?? 'Something went wrong',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.red[600],
|
||||
),
|
||||
style: TextStyle(fontSize: 16, color: Colors.red[600]),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
@ -104,10 +102,55 @@ 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) {
|
||||
final invitationKey = invitation.id.toString();
|
||||
bool isOwned = section == 'created';
|
||||
bool isAccepted = section == 'accepted';
|
||||
|
||||
bool isAccepting = _acceptingInvitations[invitationKey] ?? false;
|
||||
bool hasBeenAccepted = _acceptedInvitations[invitationKey] ?? false;
|
||||
AnimationController? animController = _animationControllers[invitationKey];
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 16),
|
||||
padding: EdgeInsets.all(20),
|
||||
@ -121,10 +164,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
border: Border.all(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
border: Border.all(color: Colors.grey.withOpacity(0.2), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -134,12 +174,16 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
Container(
|
||||
padding: EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: InvitationUtils.getColorFromHex(invitation.tag.colorHex).withOpacity(0.1),
|
||||
color: InvitationUtils.getColorFromHex(
|
||||
invitation.tag.colorHex,
|
||||
).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
InvitationUtils.getIconFromName(invitation.tag.iconName),
|
||||
color: InvitationUtils.getColorFromHex(invitation.tag.colorHex),
|
||||
color: InvitationUtils.getColorFromHex(
|
||||
invitation.tag.colorHex,
|
||||
),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
@ -158,10 +202,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
),
|
||||
Text(
|
||||
'by ${invitation.creator.displayName}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -169,7 +210,9 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: InvitationUtils.getColorFromHex(invitation.tag.colorHex).withOpacity(0.1),
|
||||
color: InvitationUtils.getColorFromHex(
|
||||
invitation.tag.colorHex,
|
||||
).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
@ -177,13 +220,16 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
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),
|
||||
Text(
|
||||
InvitationUtils.truncateDescription(invitation.description),
|
||||
@ -202,10 +248,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
invitation.location!,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -218,10 +261,7 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
InvitationUtils.getRelativeDateTime(invitation.dateTime!),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -233,22 +273,22 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: InvitationUtils.getParticipantsStatusColor(
|
||||
invitation.currentAttendees,
|
||||
invitation.maxParticipants
|
||||
invitation.currentAttendees,
|
||||
invitation.maxParticipants,
|
||||
).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
InvitationUtils.getParticipantsStatus(
|
||||
invitation.currentAttendees,
|
||||
invitation.maxParticipants
|
||||
invitation.currentAttendees,
|
||||
invitation.maxParticipants,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: InvitationUtils.getParticipantsStatusColor(
|
||||
invitation.currentAttendees,
|
||||
invitation.maxParticipants
|
||||
invitation.currentAttendees,
|
||||
invitation.maxParticipants,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -265,28 +305,65 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
width: double.infinity,
|
||||
height: 44,
|
||||
child: ElevatedButton(
|
||||
onPressed: isOwned || isAccepted ? null : () {
|
||||
// TODO: Implement accept invitation
|
||||
},
|
||||
onPressed: isOwned || isAccepted || isAccepting || hasBeenAccepted
|
||||
? null
|
||||
: () {
|
||||
_acceptInvitation(invitation);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isAccepted
|
||||
backgroundColor: (isAccepted || hasBeenAccepted)
|
||||
? Colors.green
|
||||
: (isOwned ? Colors.grey[400] : Color(0xFF6A4C93)),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
elevation: isOwned || isAccepted ? 0 : 2,
|
||||
),
|
||||
child: Text(
|
||||
isAccepted
|
||||
? 'Accepted ✓'
|
||||
: (isOwned ? 'View/Edit' : 'Accept Invite'),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
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(
|
||||
'Accepted ✓',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: Text(
|
||||
(isAccepted || hasBeenAccepted)
|
||||
? 'Accepted ✓'
|
||||
: (isOwned ? 'View/Edit' : 'Accept Invite'),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -294,9 +371,13 @@ 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();
|
||||
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -311,10 +392,14 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
...invitations.map((invitation) => Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: _buildInvitationCard(invitation, section),
|
||||
)).toList(),
|
||||
...invitations
|
||||
.map(
|
||||
(invitation) => Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: _buildInvitationCard(invitation, section),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -336,46 +421,43 @@ class _InvitationsPageState extends State<InvitationsPage> {
|
||||
),
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.refresh),
|
||||
onPressed: _loadInvitations,
|
||||
),
|
||||
IconButton(icon: Icon(Icons.refresh), onPressed: _loadInvitations),
|
||||
],
|
||||
),
|
||||
body: _isLoading
|
||||
? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93)))
|
||||
: _errorMessage != null
|
||||
? _buildErrorState()
|
||||
: _invitationsData?.isEmpty ?? true
|
||||
? _buildEmptyState()
|
||||
: RefreshIndicator(
|
||||
onRefresh: _loadInvitations,
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
? _buildErrorState()
|
||||
: _invitationsData?.isEmpty ?? true
|
||||
? _buildEmptyState()
|
||||
: RefreshIndicator(
|
||||
onRefresh: _loadInvitations,
|
||||
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: () {
|
||||
Navigator.push(
|
||||
|
||||
@ -6,21 +6,21 @@ import 'http_service.dart';
|
||||
class InvitationsService {
|
||||
static Future<Map<String, dynamic>> getAllInvitations() async {
|
||||
try {
|
||||
final response = await HttpService.get(ApiConstants.getAllInvitationsEndpoint);
|
||||
final response = await HttpService.get(
|
||||
ApiConstants.getAllInvitationsEndpoint,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = jsonDecode(response.body);
|
||||
final invitationsResponse = InvitationsResponse.fromJson(responseData);
|
||||
|
||||
|
||||
if (invitationsResponse.status) {
|
||||
return {
|
||||
'success': true,
|
||||
'data': invitationsResponse.data,
|
||||
};
|
||||
return {'success': true, 'data': invitationsResponse.data};
|
||||
} else {
|
||||
return {
|
||||
'success': false,
|
||||
'message': invitationsResponse.message ?? 'Failed to fetch invitations',
|
||||
'message':
|
||||
invitationsResponse.message ?? 'Failed to fetch invitations',
|
||||
};
|
||||
}
|
||||
} 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.',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,7 +71,8 @@ class InvitationUtils {
|
||||
return 'today at $timeString';
|
||||
} else if (eventDate == tomorrow) {
|
||||
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'];
|
||||
return '${weekdays[eventDate.weekday - 1]} at $timeString';
|
||||
} else {
|
||||
@ -94,12 +95,7 @@ class InvitationUtils {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -112,4 +108,4 @@ class InvitationUtils {
|
||||
return Colors.blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user