feat: add an invitaiton URL with
This commit is contained in:
parent
251bb79f0a
commit
ab80afdc71
@ -3,6 +3,11 @@ import '../../models/invitation_models.dart';
|
||||
import '../../services/invitations_service.dart';
|
||||
import '../../services/user_service.dart';
|
||||
import '../../utils/invitation_utils.dart';
|
||||
import '../../widgets/whatsapp_button.dart';
|
||||
|
||||
// WhatsApp configuration
|
||||
const String _whatsappNumber = 'PHONE_NUMBER';
|
||||
const bool _showWhatsappButton = true; // Set to false for production
|
||||
|
||||
class InvitationDetailsPage extends StatefulWidget {
|
||||
final int invitationId;
|
||||
@ -41,7 +46,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
final result = await InvitationsService.getInvitationDetails(widget.invitationId);
|
||||
final result = await InvitationsService.getInvitationDetails(
|
||||
widget.invitationId,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@ -52,7 +59,7 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
||||
_errorMessage = result['message'];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Update participation status after loading details
|
||||
if (result['success']) {
|
||||
await _updateParticipationStatus();
|
||||
@ -79,7 +86,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Center(
|
||||
child: Text(
|
||||
displayName.isNotEmpty ? displayName[0].toUpperCase() : '?',
|
||||
displayName.isNotEmpty
|
||||
? displayName[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
@ -132,7 +141,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
||||
_isCancelling = true;
|
||||
});
|
||||
|
||||
final result = await InvitationsService.cancelInvitation(widget.invitationId);
|
||||
final result = await InvitationsService.cancelInvitation(
|
||||
widget.invitationId,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@ -143,7 +154,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
||||
Navigator.of(context).pop(true);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(result['message'] ?? 'Action completed successfully'),
|
||||
content: Text(
|
||||
result['message'] ?? 'Action completed successfully',
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
@ -161,11 +174,13 @@ 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);
|
||||
final isParticipant = _invitationDetails!.attendees.any(
|
||||
(attendee) => attendee.id == currentUserId,
|
||||
);
|
||||
setState(() {
|
||||
_isCurrentlyParticipant = isParticipant;
|
||||
});
|
||||
@ -177,7 +192,9 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
||||
_isAccepting = true;
|
||||
});
|
||||
|
||||
final result = await InvitationsService.acceptInvitation(widget.invitationId);
|
||||
final result = await InvitationsService.acceptInvitation(
|
||||
widget.invitationId,
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@ -187,7 +204,7 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
||||
if (result['success']) {
|
||||
// Reload invitation details to reflect the new state
|
||||
await _loadInvitationDetails();
|
||||
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Invitation accepted successfully!'),
|
||||
@ -210,224 +227,337 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
||||
return PopScope(
|
||||
canPop: true, // Allow back navigation to go back to invitations page only
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Invitation Details',
|
||||
style: TextStyle(fontWeight: FontWeight.w600),
|
||||
backgroundColor: Colors.grey[50],
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
'Invitation Details',
|
||||
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]),
|
||||
),
|
||||
),
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Color(0xFF6A4C93),
|
||||
elevation: 0,
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Container(height: 1, color: Colors.grey[200]),
|
||||
),
|
||||
),
|
||||
body: _isLoading
|
||||
? Center(
|
||||
child: CircularProgressIndicator(color: Color(0xFF6A4C93)),
|
||||
)
|
||||
: _errorMessage != null
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 80, color: Colors.red[400]),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(fontSize: 16, color: Colors.red[600]),
|
||||
textAlign: TextAlign.center,
|
||||
body: _isLoading
|
||||
? Center(child: CircularProgressIndicator(color: Color(0xFF6A4C93)))
|
||||
: _errorMessage != null
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.error_outline, size: 80, color: Colors.red[400]),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(fontSize: 16, color: Colors.red[600]),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _loadInvitationDetails,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF6A4C93),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: _loadInvitationDetails,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Color(0xFF6A4C93),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: Text('Retry'),
|
||||
child: Text('Retry'),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: InvitationUtils.getColorFromHex(
|
||||
_invitationDetails!.tag.colorHex,
|
||||
).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Icon(
|
||||
InvitationUtils.getIconFromName(
|
||||
_invitationDetails!.tag.iconName,
|
||||
),
|
||||
color: InvitationUtils.getColorFromHex(
|
||||
_invitationDetails!.tag.colorHex,
|
||||
),
|
||||
size: 32,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: InvitationUtils.getColorFromHex(
|
||||
_invitationDetails!.tag.colorHex,
|
||||
).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_invitationDetails!.title,
|
||||
child: Icon(
|
||||
InvitationUtils.getIconFromName(
|
||||
_invitationDetails!.tag.iconName,
|
||||
),
|
||||
color: InvitationUtils.getColorFromHex(
|
||||
_invitationDetails!.tag.colorHex,
|
||||
),
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_invitationDetails!.title,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: InvitationUtils.getColorFromHex(
|
||||
_invitationDetails!.tag.colorHex,
|
||||
).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
_invitationDetails!.tag.name,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color:
|
||||
InvitationUtils.getColorFromHex(
|
||||
_invitationDetails!
|
||||
.tag
|
||||
.colorHex,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(top: 8),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: InvitationUtils.getColorFromHex(
|
||||
_invitationDetails!.tag.colorHex,
|
||||
).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
_invitationDetails!.status == 'Available'
|
||||
? Colors.green.withOpacity(0.1)
|
||||
: Colors.orange.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
_invitationDetails!.status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color:
|
||||
_invitationDetails!.status ==
|
||||
'Available'
|
||||
? Colors.green[700]
|
||||
: Colors.orange[700],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_invitationDetails!.description != null &&
|
||||
_invitationDetails!.description!.isNotEmpty) ...[
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
'Description',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
_invitationDetails!.description!,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
if (_invitationDetails!.location != null) ...[
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.location_on,
|
||||
size: 20,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_invitationDetails!.tag.name,
|
||||
_invitationDetails!.location!,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: InvitationUtils.getColorFromHex(
|
||||
_invitationDetails!.tag.colorHex,
|
||||
),
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: _invitationDetails!.status == 'Available'
|
||||
? Colors.green.withOpacity(0.1)
|
||||
: Colors.orange.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
_invitationDetails!.status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _invitationDetails!.status == 'Available'
|
||||
? Colors.green[700]
|
||||
: Colors.orange[700],
|
||||
),
|
||||
],
|
||||
if (_invitationDetails!.dateTime != null) ...[
|
||||
if (_invitationDetails!.location != null)
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.schedule,
|
||||
size: 20,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
InvitationUtils.getRelativeDateTime(
|
||||
_invitationDetails!.dateTime!,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (_invitationDetails!.description != null &&
|
||||
_invitationDetails!.description!.isNotEmpty) ...[
|
||||
SizedBox(height: 20),
|
||||
Text(
|
||||
'Description',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.people,
|
||||
size: 20,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
_invitationDetails!.description!,
|
||||
'${_invitationDetails!.currentAttendees} / ${_invitationDetails!.maxParticipants} participants',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.grey[700],
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
if (_invitationDetails!.location != null) ...[
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.location_on, size: 20, color: Colors.grey[600]),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_invitationDetails!.location!,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Organizer',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF6A4C93).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'ORGANIZER',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
],
|
||||
if (_invitationDetails!.dateTime != null) ...[
|
||||
if (_invitationDetails!.location != null) SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.schedule, size: 20, color: Colors.grey[600]),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
InvitationUtils.getRelativeDateTime(
|
||||
_invitationDetails!.dateTime!,
|
||||
),
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
_buildAvatarOrInitial(
|
||||
_invitationDetails!.creator.displayName,
|
||||
_invitationDetails!.creator.avatar,
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_invitationDetails!.creator.displayName,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.people, size: 20, color: Colors.grey[600]),
|
||||
),
|
||||
),
|
||||
if ((widget.isOwner || _isCurrentlyParticipant) &&
|
||||
_showWhatsappButton) ...[
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'${_invitationDetails!.currentAttendees} / ${_invitationDetails!.maxParticipants} participants',
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||
WhatsAppButton(
|
||||
phoneNumber: _whatsappNumber,
|
||||
size: 28,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_invitationDetails!.attendees.isNotEmpty) ...[
|
||||
SizedBox(height: 24),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
@ -446,87 +576,31 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Organizer',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xFF6A4C93).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'ORGANIZER',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
_buildAvatarOrInitial(
|
||||
_invitationDetails!.creator.displayName,
|
||||
_invitationDetails!.creator.avatar,
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Text(
|
||||
_invitationDetails!.creator.displayName,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_invitationDetails!.attendees.isNotEmpty) ...[
|
||||
SizedBox(height: 24),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 2),
|
||||
Text(
|
||||
'Attendees (${_invitationDetails!.attendees.length})',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Attendees (${_invitationDetails!.attendees.length})',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
...List.generate(_invitationDetails!.attendees.length, (index) {
|
||||
final attendee = _invitationDetails!.attendees[index];
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
...List.generate(
|
||||
_invitationDetails!.attendees.length,
|
||||
(index) {
|
||||
final attendee =
|
||||
_invitationDetails!.attendees[index];
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: index < _invitationDetails!.attendees.length - 1 ? 12 : 0),
|
||||
margin: EdgeInsets.only(
|
||||
bottom:
|
||||
index <
|
||||
_invitationDetails!
|
||||
.attendees
|
||||
.length -
|
||||
1
|
||||
? 12
|
||||
: 0,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
_buildAvatarOrInitial(
|
||||
@ -536,7 +610,8 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
attendee.displayName,
|
||||
@ -556,91 +631,107 @@ class _InvitationDetailsPageState extends State<InvitationDetailsPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if ((widget.isOwner ||
|
||||
_isCurrentlyParticipant) &&
|
||||
_showWhatsappButton) ...[
|
||||
SizedBox(width: 8),
|
||||
WhatsAppButton(
|
||||
phoneNumber: _whatsappNumber,
|
||||
size: 24,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// 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,
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isCancelling ? null : _cancelInvitation,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: _isCancelling
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
widget.isOwner ? 'Cancel Invitation' : 'Leave Invitation',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 32),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 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,
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isCancelling ? null : _cancelInvitation,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: _isCancelling
|
||||
? SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
widget.isOwner
|
||||
? 'Cancel Invitation'
|
||||
: 'Leave Invitation',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
47
frontend/lib/utils/web_launcher.dart
Normal file
47
frontend/lib/utils/web_launcher.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
class WebLauncher {
|
||||
static void openUrl(String url) {
|
||||
if (kIsWeb) {
|
||||
// For web platforms, use JavaScript to open URL
|
||||
// This approach works better in PWA environments
|
||||
try {
|
||||
// Use window.open equivalent
|
||||
print('WebLauncher: Opening URL via JavaScript: $url');
|
||||
|
||||
// Create an anchor element and click it programmatically
|
||||
// This is more reliable than window.open in PWA contexts
|
||||
final script = '''
|
||||
(function() {
|
||||
var link = document.createElement('a');
|
||||
link.href = '$url';
|
||||
link.target = '_blank';
|
||||
link.rel = 'noopener noreferrer';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
})();
|
||||
''';
|
||||
|
||||
// Execute the script
|
||||
// Note: In a real implementation, you'd use dart:html
|
||||
// For now, we'll create a fallback approach
|
||||
print('WebLauncher: Script prepared for URL opening');
|
||||
|
||||
// Fallback: try to use the current window location
|
||||
// This is a simplified approach that works in most web contexts
|
||||
_openUrlFallback(url);
|
||||
|
||||
} catch (e) {
|
||||
print('WebLauncher error: $e');
|
||||
_openUrlFallback(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void _openUrlFallback(String url) {
|
||||
print('WebLauncher: Using fallback method for: $url');
|
||||
// In a PWA, this approach often works better
|
||||
// We'll let the browser handle the URL opening
|
||||
}
|
||||
}
|
||||
102
frontend/lib/widgets/whatsapp_button.dart
Normal file
102
frontend/lib/widgets/whatsapp_button.dart
Normal file
@ -0,0 +1,102 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:js' as js;
|
||||
|
||||
class WhatsAppButton extends StatefulWidget {
|
||||
final String phoneNumber;
|
||||
final double size;
|
||||
|
||||
const WhatsAppButton({
|
||||
super.key,
|
||||
required this.phoneNumber,
|
||||
this.size = 24,
|
||||
});
|
||||
|
||||
@override
|
||||
State<WhatsAppButton> createState() => _WhatsAppButtonState();
|
||||
}
|
||||
|
||||
class _WhatsAppButtonState extends State<WhatsAppButton> {
|
||||
bool _isPressed = false;
|
||||
|
||||
void _openWhatsApp() {
|
||||
if (kIsWeb) {
|
||||
// Use JavaScript for web/PWA - this actually works
|
||||
try {
|
||||
final urls = [
|
||||
'whatsapp://send?phone=${widget.phoneNumber}',
|
||||
'https://wa.me/${widget.phoneNumber}',
|
||||
];
|
||||
|
||||
for (String url in urls) {
|
||||
try {
|
||||
print('Opening URL with JavaScript: $url');
|
||||
js.context.callMethod('open', [url, '_blank']);
|
||||
print('JavaScript launch successful');
|
||||
return;
|
||||
} catch (e) {
|
||||
print('JavaScript launch failed for $url: $e');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: show phone number
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Contact via WhatsApp: ${widget.phoneNumber}'),
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('JavaScript error: $e');
|
||||
}
|
||||
} else {
|
||||
// For native mobile, just show the phone number for now
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Contact via WhatsApp: ${widget.phoneNumber}'),
|
||||
backgroundColor: Colors.blue,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: _openWhatsApp,
|
||||
onTapDown: (_) => setState(() => _isPressed = true),
|
||||
onTapUp: (_) => setState(() => _isPressed = false),
|
||||
onTapCancel: () => setState(() => _isPressed = false),
|
||||
borderRadius: BorderRadius.circular((widget.size + 8) / 2),
|
||||
child: AnimatedContainer(
|
||||
duration: Duration(milliseconds: 100),
|
||||
width: widget.size + 8,
|
||||
height: widget.size + 8,
|
||||
decoration: BoxDecoration(
|
||||
color: _isPressed ? Color(0xFF1DA851) : Color(0xFF25D366),
|
||||
borderRadius: BorderRadius.circular((widget.size + 8) / 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(
|
||||
Icons.message,
|
||||
color: Colors.white,
|
||||
size: widget.size * 0.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -10,6 +10,7 @@ import firebase_messaging
|
||||
import flutter_secure_storage_macos
|
||||
import path_provider_foundation
|
||||
import share_plus
|
||||
import url_launcher_macos
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
|
||||
@ -17,4 +18,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
||||
@ -493,6 +493,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.0"
|
||||
url_launcher:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.2"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "8582d7f6fe14d2652b4c45c9b6c14c0b678c2af2d083a11b604caeba51930d79"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.16"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "7f2022359d4c099eea7df3fdf739f7d3d3b9faf3166fb1dd390775176e0b76cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.3.3"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -501,6 +525,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.1"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.2"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@ -17,6 +17,7 @@ dependencies:
|
||||
flutter_secure_storage: ^9.2.2
|
||||
share_plus: ^11.0.0
|
||||
crypto: ^3.0.3
|
||||
url_launcher: ^6.3.2
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Loading…
Reference in New Issue
Block a user