diff --git a/frontend/lib/screens/home_screen.dart b/frontend/lib/screens/home_screen.dart index a856e0c..52a225e 100644 --- a/frontend/lib/screens/home_screen.dart +++ b/frontend/lib/screens/home_screen.dart @@ -2,7 +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 'pages/puzzles_page.dart'; import '../services/invitations_service.dart'; class HomeScreen extends StatefulWidget { @@ -17,7 +17,7 @@ class _HomeScreenState extends State { final List _pages = [ FeedPage(), InvitationsPage(), - WordlePage(), + PuzzlesPage(), ProfilePage(), ]; diff --git a/frontend/lib/screens/pages/leaderboard_page.dart b/frontend/lib/screens/pages/leaderboard_page.dart new file mode 100644 index 0000000..6003b3c --- /dev/null +++ b/frontend/lib/screens/pages/leaderboard_page.dart @@ -0,0 +1,280 @@ +import 'package:flutter/material.dart'; + +class LeaderboardPage extends StatelessWidget { + LeaderboardPage({Key? key}) : super(key: key); + + // Sample JSON data structure for leaderboard + final List> leaderboardData = [ + { + "user": { + "displayName": "Ahmed Hassan", + "avatar": "https://via.placeholder.com/50", + }, + "attempts": 3, + "time": "2025-01-15T08:45:23.000Z", + }, + { + "user": { + "displayName": "Sarah Abdullah", + "avatar": "https://via.placeholder.com/50", + }, + "attempts": 4, + "time": "2025-01-15T09:12:45.000Z", + }, + { + "user": { + "displayName": "Omar Al-Rashid", + "avatar": "https://via.placeholder.com/50", + }, + "attempts": 2, + "time": "2025-01-15T08:23:12.000Z", + }, + { + "user": { + "displayName": "Fatima Al-Zahra", + "avatar": "https://via.placeholder.com/50", + }, + "attempts": 5, + "time": "2025-01-15T09:34:56.000Z", + }, + { + "user": { + "displayName": "Khalid Mohammed", + "avatar": "https://via.placeholder.com/50", + }, + "attempts": 3, + "time": "2025-01-15T08:56:34.000Z", + }, + { + "user": { + "displayName": "Layla Ibrahim", + "avatar": "https://via.placeholder.com/50", + }, + "attempts": 4, + "time": "2025-01-15T09:18:27.000Z", + }, + ]; + + @override + Widget build(BuildContext context) { + // Sort leaderboard by time (fastest first) + final sortedData = List>.from(leaderboardData); + sortedData.sort((a, b) { + final timeA = DateTime.parse(a['time']); + final timeB = DateTime.parse(b['time']); + return timeA.compareTo(timeB); + }); + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: Container(), + 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( + 'Leaderboard', + 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: [ + // Leaderboard list + Expanded( + child: ListView.builder( + padding: EdgeInsets.all(16), + itemCount: sortedData.length, + itemBuilder: (context, index) { + final entry = sortedData[index]; + final user = entry['user']; + final attempts = entry['attempts']; + final timeString = entry['time']; + final time = DateTime.parse(timeString); + final rank = index + 1; + + // Format time display + final timeFormatted = + '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}'; + + // Determine rank styling + Color rankColor = Colors.grey[600]!; + IconData? rankIcon; + if (rank == 1) { + rankColor = Color(0xFFFFD700); // Gold + rankIcon = Icons.emoji_events; + } else if (rank == 2) { + rankColor = Color(0xFFC0C0C0); // Silver + rankIcon = Icons.emoji_events; + } else if (rank == 3) { + rankColor = Color(0xFFCD7F32); // Bronze + rankIcon = Icons.emoji_events; + } + + return Container( + margin: EdgeInsets.only(bottom: 12), + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + border: rank <= 3 + ? Border.all( + color: rankColor.withOpacity(0.3), + width: 2, + ) + : Border.all(color: Colors.grey[200]!, width: 1), + ), + child: Row( + children: [ + // Rank indicator + Container( + width: 40, + child: Column( + children: [ + if (rankIcon != null) + Icon(rankIcon, color: rankColor, size: 24) + else + Text( + '#$rank', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: rankColor, + ), + ), + ], + ), + ), + SizedBox(width: 16), + + // User avatar + CircleAvatar( + radius: 25, + backgroundColor: Color(0xFF6A4C93).withOpacity(0.1), + child: Text( + user['displayName'][0].toUpperCase(), + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF6A4C93), + ), + ), + ), + SizedBox(width: 16), + + // User details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + user['displayName'], + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.access_time, + size: 16, + color: Colors.grey[600], + ), + SizedBox(width: 4), + Text( + timeFormatted, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + SizedBox(width: 16), + Icon( + Icons.try_sms_star, + size: 16, + color: Colors.grey[600], + ), + SizedBox(width: 4), + Text( + '$attempts attempts', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + }, + ), + ), + + // Return button + Container( + padding: EdgeInsets.all(24), + child: SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () { + Navigator.pop(context); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Color(0xFF6A4C93), + padding: EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + child: Text( + 'Return', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/frontend/lib/screens/pages/puzzles_page.dart b/frontend/lib/screens/pages/puzzles_page.dart new file mode 100644 index 0000000..5db1cea --- /dev/null +++ b/frontend/lib/screens/pages/puzzles_page.dart @@ -0,0 +1,444 @@ +import 'package:flutter/material.dart'; +import 'wordle_page.dart'; + +class PuzzlesPage extends StatefulWidget { + const PuzzlesPage({Key? key}) : super(key: key); + + @override + _PuzzlesPageState createState() => _PuzzlesPageState(); +} + +class _PuzzlesPageState extends State { + String _currentView = 'main'; // main, leaderboard + + void _showLeaderboardView() { + setState(() { + _currentView = 'leaderboard'; + }); + } + + void _showMainView() { + setState(() { + _currentView = 'main'; + }); + } + + @override + Widget build(BuildContext context) { + if (_currentView == 'leaderboard') { + return _buildLeaderboardView(); + } + return _buildMainView(); + } + + Widget _buildMainView() { + return Scaffold( + backgroundColor: Colors.white, + body: SafeArea( + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF32B0A5), Color(0xFF4600B9)], + ), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // App Logo + Text( + 'وصال', + style: TextStyle( + fontSize: 72, + fontWeight: FontWeight.w200, + fontFamily: 'Blaka', + color: Colors.white, + ), + ), + SizedBox(height: 40), + + // Daily Challenge Title + Text( + 'Daily Challenge', + style: TextStyle( + fontSize: 36, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + SizedBox(height: 16), + + // Challenge timing info + Container( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + 'Daily Wordle Challenge Open Until 11 AM', + style: TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + ), + ), + SizedBox(height: 60), + + // Big gamified PLAY button + GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (context) => WordlePage()), + ); + }, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 40, vertical: 20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.white, Colors.white.withOpacity(0.9)], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(25), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.3), + blurRadius: 20, + offset: Offset(0, 8), + ), + BoxShadow( + color: Colors.white.withOpacity(0.5), + blurRadius: 10, + offset: Offset(-5, -5), + ), + ], + border: Border.all( + color: Color(0xFF6A4C93).withOpacity(0.2), + width: 2, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.sports_esports, + size: 32, + color: Color(0xFF6A4C93), + ), + SizedBox(width: 12), + Text( + 'PLAY NOW', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Color(0xFF6A4C93), + letterSpacing: 1.5, + ), + ), + SizedBox(width: 8), + Icon( + Icons.arrow_forward_ios, + size: 20, + color: Color(0xFF6A4C93), + ), + ], + ), + ), + ), + SizedBox(height: 40), + + // Leaderboard button + GestureDetector( + onTap: _showLeaderboardView, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(30), + border: Border.all(color: Colors.white, width: 2), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.leaderboard, color: Colors.white, size: 24), + SizedBox(width: 8), + Text( + 'Leaderboard', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildLeaderboardView() { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: Color(0xFF6A4C93)), + onPressed: _showMainView, + ), + 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( + 'Leaderboard', + 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: _LeaderboardContent(), + ); + } +} + +// Wrapper class for Leaderboard content without the scaffold +class _LeaderboardContent extends StatelessWidget { + final List> leaderboardData = [ + { + "user": { + "displayName": "Ahmed Hassan", + "avatar": "https://via.placeholder.com/50", + }, + "attempts": 3, + "time": "2025-01-15T08:45:23.000Z", + }, + { + "user": { + "displayName": "Sarah Abdullah", + "avatar": "https://via.placeholder.com/50", + }, + "attempts": 4, + "time": "2025-01-15T09:12:45.000Z", + }, + { + "user": { + "displayName": "Omar Al-Rashid", + "avatar": "https://via.placeholder.com/50", + }, + "attempts": 2, + "time": "2025-01-15T08:23:12.000Z", + }, + { + "user": { + "displayName": "Fatima Al-Zahra", + "avatar": "https://via.placeholder.com/50", + }, + "attempts": 5, + "time": "2025-01-15T09:34:56.000Z", + }, + { + "user": { + "displayName": "Khalid Mohammed", + "avatar": "https://via.placeholder.com/50", + }, + "attempts": 3, + "time": "2025-01-15T08:56:34.000Z", + }, + { + "user": { + "displayName": "Layla Ibrahim", + "avatar": "https://via.placeholder.com/50", + }, + "attempts": 4, + "time": "2025-01-15T09:18:27.000Z", + }, + ]; + + @override + Widget build(BuildContext context) { + // Sort leaderboard by time (fastest first) + final sortedData = List>.from(leaderboardData); + sortedData.sort((a, b) { + final timeA = DateTime.parse(a['time']); + final timeB = DateTime.parse(b['time']); + return timeA.compareTo(timeB); + }); + + return Column( + children: [ + // Leaderboard list + Expanded( + child: ListView.builder( + padding: EdgeInsets.all(16), + itemCount: sortedData.length, + itemBuilder: (context, index) { + final entry = sortedData[index]; + final user = entry['user']; + final attempts = entry['attempts']; + final timeString = entry['time']; + final time = DateTime.parse(timeString); + final rank = index + 1; + + // Format time display + final timeFormatted = + '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}'; + + // Determine rank styling + Color rankColor = Colors.grey[600]!; + IconData? rankIcon; + if (rank == 1) { + rankColor = Color(0xFFFFD700); // Gold + rankIcon = Icons.emoji_events; + } else if (rank == 2) { + rankColor = Color(0xFFC0C0C0); // Silver + rankIcon = Icons.emoji_events; + } else if (rank == 3) { + rankColor = Color(0xFFCD7F32); // Bronze + rankIcon = Icons.emoji_events; + } + + return Container( + margin: EdgeInsets.only(bottom: 12), + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(12), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + border: rank <= 3 + ? Border.all(color: rankColor.withOpacity(0.3), width: 2) + : Border.all(color: Colors.grey[200]!, width: 1), + ), + child: Row( + children: [ + // Rank indicator + Container( + width: 40, + child: Column( + children: [ + if (rankIcon != null) + Icon(rankIcon, color: rankColor, size: 24) + else + Text( + '#$rank', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: rankColor, + ), + ), + ], + ), + ), + SizedBox(width: 16), + + // User avatar + CircleAvatar( + radius: 25, + backgroundColor: Color(0xFF6A4C93).withOpacity(0.1), + child: Text( + user['displayName'][0].toUpperCase(), + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Color(0xFF6A4C93), + ), + ), + ), + SizedBox(width: 16), + + // User details + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + user['displayName'], + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.black87, + ), + ), + SizedBox(height: 4), + Row( + children: [ + Icon( + Icons.access_time, + size: 16, + color: Colors.grey[600], + ), + SizedBox(width: 4), + Text( + timeFormatted, + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + SizedBox(width: 16), + Icon( + Icons.try_sms_star, + size: 16, + color: Colors.grey[600], + ), + SizedBox(width: 4), + Text( + '$attempts attempts', + style: TextStyle( + fontSize: 14, + color: Colors.grey[600], + ), + ), + ], + ), + ], + ), + ), + ], + ), + ); + }, + ), + ), + ], + ); + } +} + +enum LetterState { empty, absent, present, correct } diff --git a/frontend/lib/screens/pages/wordle_page.dart b/frontend/lib/screens/pages/wordle_page.dart index d03e75c..7c7899a 100644 --- a/frontend/lib/screens/pages/wordle_page.dart +++ b/frontend/lib/screens/pages/wordle_page.dart @@ -255,7 +255,10 @@ class _WordlePageState extends State with TickerProviderStateMixin { appBar: AppBar( backgroundColor: Colors.white, elevation: 0, - automaticallyImplyLeading: false, + leading: IconButton( + icon: Icon(Icons.arrow_back, color: Color(0xFF6A4C93)), + onPressed: () => Navigator.pop(context), + ), centerTitle: true, title: Row( mainAxisSize: MainAxisSize.min,