commit
8104411066
@ -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 'pages/wordle_page.dart';
|
||||
import '../services/invitations_service.dart';
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
@ -13,7 +14,12 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
int _currentIndex = 0;
|
||||
int _availableInvitationsCount = 0;
|
||||
|
||||
final List<Widget> _pages = [FeedPage(), InvitationsPage(), ProfilePage()];
|
||||
final List<Widget> _pages = [
|
||||
FeedPage(),
|
||||
InvitationsPage(),
|
||||
WordlePage(),
|
||||
ProfilePage(),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -43,7 +49,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
if (_availableInvitationsCount == 0) {
|
||||
return Icon(Icons.mail);
|
||||
}
|
||||
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
Icon(Icons.mail),
|
||||
@ -56,12 +62,11 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
color: Colors.red,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
minWidth: 16,
|
||||
minHeight: 16,
|
||||
),
|
||||
constraints: BoxConstraints(minWidth: 16, minHeight: 16),
|
||||
child: Text(
|
||||
_availableInvitationsCount > 99 ? '99+' : '$_availableInvitationsCount',
|
||||
_availableInvitationsCount > 99
|
||||
? '99+'
|
||||
: '$_availableInvitationsCount',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
@ -108,12 +113,22 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
backgroundColor: Colors.transparent,
|
||||
elevation: 0,
|
||||
items: [
|
||||
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Feed'),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.home),
|
||||
label: 'Feed',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: _buildInvitationsBadge(),
|
||||
label: 'Invitations',
|
||||
),
|
||||
BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.games),
|
||||
label: 'Puzzles',
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: Icon(Icons.person),
|
||||
label: 'Profile',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
599
frontend/lib/screens/pages/wordle_page.dart
Normal file
599
frontend/lib/screens/pages/wordle_page.dart
Normal file
@ -0,0 +1,599 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'dart:io';
|
||||
|
||||
class WordlePage extends StatefulWidget {
|
||||
@override
|
||||
_WordlePageState createState() => _WordlePageState();
|
||||
}
|
||||
|
||||
class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
|
||||
static const String TARGET_WORD = "HELLO";
|
||||
static const int MAX_ATTEMPTS = 6;
|
||||
static const int WORD_LENGTH = 5;
|
||||
|
||||
List<List<String>> grid = List.generate(
|
||||
MAX_ATTEMPTS,
|
||||
(_) => List.filled(WORD_LENGTH, ''),
|
||||
);
|
||||
List<List<LetterState>> gridStates = List.generate(
|
||||
MAX_ATTEMPTS,
|
||||
(_) => List.filled(WORD_LENGTH, LetterState.empty),
|
||||
);
|
||||
Map<String, LetterState> keyboardStates = {};
|
||||
|
||||
int currentRow = 0;
|
||||
int currentCol = 0;
|
||||
int animatingRow = -1;
|
||||
bool gameWon = false;
|
||||
bool gameLost = false;
|
||||
|
||||
late AnimationController _revealController;
|
||||
late List<Animation<double>> _tileAnimations;
|
||||
late FocusNode _focusNode;
|
||||
Set<LogicalKeyboardKey> _pressedKeys = <LogicalKeyboardKey>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeAnimations();
|
||||
_initializeKeyboardStates();
|
||||
_focusNode = FocusNode();
|
||||
|
||||
// Request focus for keyboard input on web/desktop
|
||||
if (kIsWeb || (!kIsWeb && !Platform.isIOS && !Platform.isAndroid)) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _initializeAnimations() {
|
||||
_revealController = AnimationController(
|
||||
duration: Duration(milliseconds: 1500),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_tileAnimations = List.generate(
|
||||
WORD_LENGTH,
|
||||
(index) => Tween<double>(begin: 0.0, end: 1.0).animate(
|
||||
CurvedAnimation(
|
||||
parent: _revealController,
|
||||
curve: Interval(
|
||||
index * 0.1,
|
||||
(index * 0.1) + 0.5,
|
||||
curve: Curves.elasticOut,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _initializeKeyboardStates() {
|
||||
for (int i = 65; i <= 90; i++) {
|
||||
keyboardStates[String.fromCharCode(i)] = LetterState.empty;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_revealController.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _addLetter(String letter) {
|
||||
if (currentCol < WORD_LENGTH &&
|
||||
currentRow < MAX_ATTEMPTS &&
|
||||
!gameWon &&
|
||||
!gameLost) {
|
||||
setState(() {
|
||||
grid[currentRow][currentCol] = letter;
|
||||
currentCol++;
|
||||
});
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
}
|
||||
|
||||
void _deleteLetter() {
|
||||
if (currentCol > 0 && !gameWon && !gameLost) {
|
||||
setState(() {
|
||||
currentCol--;
|
||||
grid[currentRow][currentCol] = '';
|
||||
});
|
||||
HapticFeedback.lightImpact();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submitWord() async {
|
||||
if (currentCol == WORD_LENGTH && !gameWon && !gameLost) {
|
||||
String guess = grid[currentRow].join('');
|
||||
List<LetterState> newStates = _evaluateGuess(guess);
|
||||
|
||||
// Set the states and start animating the current row
|
||||
setState(() {
|
||||
gridStates[currentRow] = newStates;
|
||||
_updateKeyboardStates(guess, newStates);
|
||||
animatingRow = currentRow;
|
||||
});
|
||||
|
||||
// Start the reveal animation
|
||||
_revealController.reset();
|
||||
await _revealController.forward();
|
||||
|
||||
// Clear the animating row and move to next row
|
||||
setState(() {
|
||||
animatingRow = -1;
|
||||
});
|
||||
|
||||
if (guess == TARGET_WORD) {
|
||||
setState(() {
|
||||
gameWon = true;
|
||||
});
|
||||
HapticFeedback.heavyImpact();
|
||||
_showGameEndDialog(true);
|
||||
} else if (currentRow == MAX_ATTEMPTS - 1) {
|
||||
setState(() {
|
||||
gameLost = true;
|
||||
});
|
||||
HapticFeedback.heavyImpact();
|
||||
_showGameEndDialog(false);
|
||||
} else {
|
||||
setState(() {
|
||||
currentRow++;
|
||||
currentCol = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<LetterState> _evaluateGuess(String guess) {
|
||||
List<LetterState> states = List.filled(WORD_LENGTH, LetterState.absent);
|
||||
List<String> targetLetters = TARGET_WORD.split('');
|
||||
List<bool> used = List.filled(WORD_LENGTH, false);
|
||||
|
||||
// First pass: check for correct positions
|
||||
for (int i = 0; i < WORD_LENGTH; i++) {
|
||||
if (guess[i] == targetLetters[i]) {
|
||||
states[i] = LetterState.correct;
|
||||
used[i] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: check for present letters
|
||||
for (int i = 0; i < WORD_LENGTH; i++) {
|
||||
if (states[i] != LetterState.correct) {
|
||||
for (int j = 0; j < WORD_LENGTH; j++) {
|
||||
if (!used[j] && guess[i] == targetLetters[j]) {
|
||||
states[i] = LetterState.present;
|
||||
used[j] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return states;
|
||||
}
|
||||
|
||||
void _updateKeyboardStates(String guess, List<LetterState> states) {
|
||||
for (int i = 0; i < guess.length; i++) {
|
||||
String letter = guess[i];
|
||||
LetterState currentState = keyboardStates[letter] ?? LetterState.empty;
|
||||
LetterState newState = states[i];
|
||||
|
||||
if (currentState == LetterState.correct) continue;
|
||||
if (currentState == LetterState.present && newState == LetterState.absent)
|
||||
continue;
|
||||
|
||||
keyboardStates[letter] = newState;
|
||||
}
|
||||
}
|
||||
|
||||
void _showGameEndDialog(bool won) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(won ? 'Congratulations!' : 'Game Over'),
|
||||
content: Text(
|
||||
won ? 'You solved today\'s puzzle!' : 'The word was: $TARGET_WORD',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
_resetGame();
|
||||
},
|
||||
child: Text(
|
||||
'Play Again',
|
||||
style: TextStyle(color: Color(0xFF6A4C93)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _resetGame() {
|
||||
setState(() {
|
||||
grid = List.generate(MAX_ATTEMPTS, (_) => List.filled(WORD_LENGTH, ''));
|
||||
gridStates = List.generate(
|
||||
MAX_ATTEMPTS,
|
||||
(_) => List.filled(WORD_LENGTH, LetterState.empty),
|
||||
);
|
||||
currentRow = 0;
|
||||
currentCol = 0;
|
||||
animatingRow = -1;
|
||||
gameWon = false;
|
||||
gameLost = false;
|
||||
_initializeKeyboardStates();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopScope(
|
||||
canPop: false,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
// Prevent mobile keyboard from showing up
|
||||
FocusScope.of(context).unfocus();
|
||||
// Only request focus on web/desktop platforms
|
||||
if (kIsWeb || (!kIsWeb && !Platform.isIOS && !Platform.isAndroid)) {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
},
|
||||
child: RawKeyboardListener(
|
||||
focusNode: _focusNode,
|
||||
onKey: _handleRawKeyEvent,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.white,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
automaticallyImplyLeading: false,
|
||||
centerTitle: true,
|
||||
title: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'وصال',
|
||||
style: TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w200,
|
||||
fontFamily: 'Blaka',
|
||||
color: Color(0xFF6A4C93),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Daily Challenge',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottom: PreferredSize(
|
||||
preferredSize: Size.fromHeight(1),
|
||||
child: Container(height: 1, color: Colors.grey[200]),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(maxWidth: 350),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Game Grid
|
||||
Container(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: List.generate(MAX_ATTEMPTS, (row) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(WORD_LENGTH, (col) {
|
||||
bool shouldAnimate =
|
||||
row == animatingRow &&
|
||||
gridStates[row][col] !=
|
||||
LetterState.empty;
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.all(2),
|
||||
width: 62,
|
||||
height: 62,
|
||||
child: shouldAnimate
|
||||
? AnimatedBuilder(
|
||||
animation: _tileAnimations[col],
|
||||
builder: (context, child) {
|
||||
double scale =
|
||||
0.8 +
|
||||
(0.2 *
|
||||
_tileAnimations[col]
|
||||
.value);
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: Container(
|
||||
width: 62,
|
||||
height: 62,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color:
|
||||
_getBorderColor(
|
||||
row,
|
||||
col,
|
||||
),
|
||||
width: 2,
|
||||
),
|
||||
color:
|
||||
_getAnimatedTileColor(
|
||||
row,
|
||||
col,
|
||||
_tileAnimations[col]
|
||||
.value,
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(
|
||||
4,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
grid[row][col],
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
color: _getAnimatedTextColor(
|
||||
row,
|
||||
col,
|
||||
_tileAnimations[col]
|
||||
.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: Container(
|
||||
width: 62,
|
||||
height: 62,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: _getBorderColor(
|
||||
row,
|
||||
col,
|
||||
),
|
||||
width: 2,
|
||||
),
|
||||
color: _getTileColor(
|
||||
row,
|
||||
col,
|
||||
),
|
||||
borderRadius:
|
||||
BorderRadius.circular(4),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
grid[row][col],
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight:
|
||||
FontWeight.bold,
|
||||
color: _getTextColor(
|
||||
row,
|
||||
col,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Keyboard
|
||||
Container(padding: EdgeInsets.all(8), child: _buildKeyboard()),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKeyboard() {
|
||||
List<List<String>> keyboardLayout = [
|
||||
['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
|
||||
['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'],
|
||||
['ENTER', 'Z', 'X', 'C', 'V', 'B', 'N', 'M', '⌫'],
|
||||
];
|
||||
|
||||
return Column(
|
||||
children: keyboardLayout.map((row) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: row.map((key) {
|
||||
bool isSpecial = key == 'ENTER' || key == '⌫';
|
||||
double width = isSpecial ? 65 : 35;
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 1.5),
|
||||
width: width,
|
||||
height: 58,
|
||||
child: Material(
|
||||
color: _getKeyColor(key),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
onTap: () => _handleKeyTap(key),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
key,
|
||||
style: TextStyle(
|
||||
fontSize: isSpecial ? 12 : 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _getKeyTextColor(key),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleKeyTap(String key) {
|
||||
if (key == 'ENTER') {
|
||||
_submitWord();
|
||||
} else if (key == '⌫') {
|
||||
_deleteLetter();
|
||||
} else {
|
||||
_addLetter(key);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleRawKeyEvent(RawKeyEvent event) {
|
||||
if (event is RawKeyDownEvent) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
_submitWord();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.backspace) {
|
||||
_deleteLetter();
|
||||
} else if (event.character != null && event.character!.isNotEmpty) {
|
||||
String key = event.character!.toUpperCase();
|
||||
if (key.codeUnitAt(0) >= 65 && key.codeUnitAt(0) <= 90) {
|
||||
_addLetter(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Color _getBorderColor(int row, int col) {
|
||||
if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.grey[300]!;
|
||||
if (row < currentRow || (row == currentRow && gameWon)) {
|
||||
return Colors.transparent;
|
||||
} else if (row == currentRow && col < currentCol) {
|
||||
return Colors.grey[600]!;
|
||||
} else {
|
||||
return Colors.grey[300]!;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getTileColor(int row, int col) {
|
||||
if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.white;
|
||||
if (row >= currentRow && !gameWon) return Colors.white;
|
||||
|
||||
switch (gridStates[row][col]) {
|
||||
case LetterState.correct:
|
||||
return Color(0xFF6AAE7C);
|
||||
case LetterState.present:
|
||||
return Color(0xFFC9B037);
|
||||
case LetterState.absent:
|
||||
return Color(0xFF787C7E);
|
||||
default:
|
||||
return Colors.white;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getTextColor(int row, int col) {
|
||||
if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.black;
|
||||
if (row >= currentRow && !gameWon) return Colors.black;
|
||||
return gridStates[row][col] == LetterState.empty
|
||||
? Colors.black
|
||||
: Colors.white;
|
||||
}
|
||||
|
||||
Color _getAnimatedTileColor(int row, int col, double animationValue) {
|
||||
if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.white;
|
||||
if (row != animatingRow || gridStates[row][col] == LetterState.empty) {
|
||||
return _getTileColor(row, col);
|
||||
}
|
||||
|
||||
// Show color based on animation progress
|
||||
Color targetColor;
|
||||
switch (gridStates[row][col]) {
|
||||
case LetterState.correct:
|
||||
targetColor = Color(0xFF6AAE7C);
|
||||
break;
|
||||
case LetterState.present:
|
||||
targetColor = Color(0xFFC9B037);
|
||||
break;
|
||||
case LetterState.absent:
|
||||
targetColor = Color(0xFF787C7E);
|
||||
break;
|
||||
default:
|
||||
targetColor = Colors.white;
|
||||
}
|
||||
|
||||
return Color.lerp(Colors.white, targetColor, animationValue) ??
|
||||
Colors.white;
|
||||
}
|
||||
|
||||
Color _getAnimatedTextColor(int row, int col, double animationValue) {
|
||||
if (row >= MAX_ATTEMPTS || col >= WORD_LENGTH) return Colors.black;
|
||||
if (row != animatingRow || gridStates[row][col] == LetterState.empty) {
|
||||
return _getTextColor(row, col);
|
||||
}
|
||||
|
||||
// Text color changes with animation
|
||||
return Color.lerp(Colors.black, Colors.white, animationValue) ??
|
||||
Colors.black;
|
||||
}
|
||||
|
||||
Color _getKeyColor(String key) {
|
||||
if (key == 'ENTER' || key == '⌫') {
|
||||
return Colors.grey[300]!;
|
||||
}
|
||||
|
||||
LetterState state = keyboardStates[key] ?? LetterState.empty;
|
||||
switch (state) {
|
||||
case LetterState.correct:
|
||||
return Color(0xFF6AAE7C);
|
||||
case LetterState.present:
|
||||
return Color(0xFFC9B037);
|
||||
case LetterState.absent:
|
||||
return Color(0xFF787C7E);
|
||||
default:
|
||||
return Colors.grey[200]!;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getKeyTextColor(String key) {
|
||||
if (key == 'ENTER' || key == '⌫') {
|
||||
return Colors.black;
|
||||
}
|
||||
|
||||
LetterState state = keyboardStates[key] ?? LetterState.empty;
|
||||
return state == LetterState.empty ? Colors.black : Colors.white;
|
||||
}
|
||||
}
|
||||
|
||||
enum LetterState { empty, absent, present, correct }
|
||||
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