From e4781c0aa215cbcb14cc35434d621d31b48a40f2 Mon Sep 17 00:00:00 2001 From: sBubshait Date: Thu, 7 Aug 2025 09:51:21 +0300 Subject: [PATCH] feat: lock the puzzles page outside of 9AM-11AM --- frontend/lib/screens/pages/puzzles_page.dart | 268 ++++++++++++++----- 1 file changed, 208 insertions(+), 60 deletions(-) diff --git a/frontend/lib/screens/pages/puzzles_page.dart b/frontend/lib/screens/pages/puzzles_page.dart index b832430..15ab575 100644 --- a/frontend/lib/screens/pages/puzzles_page.dart +++ b/frontend/lib/screens/pages/puzzles_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'dart:async'; import 'wordle_page.dart'; import '../../services/puzzle_service.dart'; import '../../services/user_service.dart'; @@ -16,6 +17,8 @@ class _PuzzlesPageState extends State { DailyChallenge? _dailyChallenge; bool _isLoading = false; String? _errorMessage; + Timer? _timer; + DateTime _currentTime = DateTime.now(); void _showLeaderboardView() { setState(() { @@ -33,6 +36,51 @@ class _PuzzlesPageState extends State { void initState() { super.initState(); _loadDailyChallenge(); + _updateCurrentTime(); + // Update time every minute + _timer = Timer.periodic(Duration(minutes: 1), (timer) { + _updateCurrentTime(); + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + void _updateCurrentTime() { + setState(() { + _currentTime = DateTime.now(); + }); + } + + DateTime get _currentTimeUTC3 { + // Convert current time to UTC+3 (Riyadh timezone) + final utc = _currentTime.toUtc(); + return utc.add(Duration(hours: 3)); + } + + bool get _isGameAvailable { + final timeUTC3 = _currentTimeUTC3; + final hour = timeUTC3.hour; + return hour >= 9 && hour < 11; // 9 AM to 11 AM UTC+3 + } + + String get _gameStatusText { + final timeUTC3 = _currentTimeUTC3; + final hour = timeUTC3.hour; + final minute = timeUTC3.minute; + final currentTimeFormatted = + '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}'; + + if (_isGameAvailable) { + return 'Game Available Now!'; + } else if (hour < 9) { + return 'Daily Challenge starts at 09:00'; + } else { + return 'Daily Challenge ended at 11:00'; + } } Future _loadDailyChallenge() async { @@ -56,6 +104,11 @@ class _PuzzlesPageState extends State { } void _startGame() async { + if (!_isGameAvailable) { + _showGameNotAvailableDialog(); + return; + } + if (_dailyChallenge == null) { _loadDailyChallenge(); return; @@ -75,6 +128,35 @@ class _PuzzlesPageState extends State { }); } + void _showGameNotAvailableDialog() { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Game Not Available'), + content: Text( + 'The Daily Challenge is only available between 09:00 and 11:00.\n\n' + 'You can still view the leaderboard to see today\'s results!', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text('OK', style: TextStyle(color: Color(0xFF6A4C93))), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(); + _showLeaderboardView(); + }, + child: Text( + 'View Leaderboard', + style: TextStyle(color: Color(0xFF6A4C93)), + ), + ), + ], + ), + ); + } + @override Widget build(BuildContext context) { if (_currentView == 'leaderboard') { @@ -122,50 +204,83 @@ class _PuzzlesPageState extends State { ), SizedBox(height: 16), - // Challenge timing info + // Challenge timing info with live status Container( padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), + color: (_isGameAvailable ? Colors.green : Colors.orange) + .withOpacity(0.3), borderRadius: BorderRadius.circular(20), - ), - child: Text( - 'Daily Wordle Challenge Open Until 11 AM', - style: TextStyle( - fontSize: 16, - color: Colors.white, - fontWeight: FontWeight.w500, + border: Border.all( + color: _isGameAvailable + ? Colors.green.shade300 + : Colors.orange.shade300, + width: 1, ), ), + child: Column( + children: [ + Text( + 'Daily Challenge: 09:00 - 11:00', + style: TextStyle( + fontSize: 16, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 4), + Text( + _gameStatusText, + style: TextStyle( + fontSize: 14, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ], + ), ), SizedBox(height: 60), - // Big gamified PLAY button + // Big gamified PLAY button with conditional availability GestureDetector( onTap: _isLoading ? null : _startGame, child: Container( padding: EdgeInsets.symmetric(horizontal: 40, vertical: 20), decoration: BoxDecoration( gradient: LinearGradient( - colors: [Colors.white, Colors.white.withOpacity(0.9)], + colors: _isGameAvailable + ? [Colors.white, Colors.white.withOpacity(0.9)] + : [Colors.grey[300]!, Colors.grey[400]!], 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), - ), - ], + boxShadow: _isGameAvailable + ? [ + 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), + ), + ] + : [ + BoxShadow( + color: Colors.black.withOpacity(0.1), + blurRadius: 5, + offset: Offset(0, 2), + ), + ], border: Border.all( - color: Color(0xFF6A4C93).withOpacity(0.2), + color: _isGameAvailable + ? Color(0xFF6A4C93).withOpacity(0.2) + : Colors.grey.withOpacity(0.3), width: 2, ), ), @@ -173,9 +288,13 @@ class _PuzzlesPageState extends State { mainAxisSize: MainAxisSize.min, children: [ Icon( - Icons.sports_esports, + _isGameAvailable + ? Icons.sports_esports + : Icons.access_time, size: 32, - color: Color(0xFF6A4C93), + color: _isGameAvailable + ? Color(0xFF6A4C93) + : Colors.grey[600], ), SizedBox(width: 12), _isLoading @@ -185,32 +304,42 @@ class _PuzzlesPageState extends State { child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( - Color(0xFF6A4C93), + _isGameAvailable + ? Color(0xFF6A4C93) + : Colors.grey[600]!, ), ), ) : Text( - _dailyChallenge?.attempted == true + !_isGameAvailable + ? 'GAME CLOSED' + : _dailyChallenge?.attempted == true ? 'VIEW RESULTS' : 'PLAY NOW', style: TextStyle( fontSize: 24, fontWeight: FontWeight.bold, - color: Color(0xFF6A4C93), + color: _isGameAvailable + ? Color(0xFF6A4C93) + : Colors.grey[600], letterSpacing: 1.5, ), ), SizedBox(width: 8), Icon( - Icons.arrow_forward_ios, + _isGameAvailable + ? Icons.arrow_forward_ios + : Icons.lock, size: 20, - color: Color(0xFF6A4C93), + color: _isGameAvailable + ? Color(0xFF6A4C93) + : Colors.grey[600], ), ], ), ), ), - + // Show error message if any if (_errorMessage != null) ...[ SizedBox(height: 20), @@ -230,14 +359,18 @@ class _PuzzlesPageState extends State { ), ), ], - + // Show completion status if attempted if (_dailyChallenge?.attempted == true) ...[ SizedBox(height: 20), Container( padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), decoration: BoxDecoration( - color: (_dailyChallenge!.solved ? Colors.green : Colors.orange).withOpacity(0.2), + color: + (_dailyChallenge!.solved + ? Colors.green + : Colors.orange) + .withOpacity(0.2), borderRadius: BorderRadius.circular(20), ), child: Text( @@ -253,7 +386,28 @@ class _PuzzlesPageState extends State { ), ), ], - + + // Game availability help text + if (!_isGameAvailable) ...[ + SizedBox(height: 20), + Container( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), + decoration: BoxDecoration( + color: Colors.blue.withOpacity(0.2), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + 'The Daily Challenge is available every day from 09:00 to 11:00 Riyadh time. You can still view the leaderboard!', + style: TextStyle( + fontSize: 14, + color: Colors.white, + fontWeight: FontWeight.w500, + ), + textAlign: TextAlign.center, + ), + ), + ], + SizedBox(height: 40), // Leaderboard button @@ -374,11 +528,12 @@ class _LeaderboardContentState extends State<_LeaderboardContent> { if (userResponse['success'] == true && userResponse['data'] != null) { _currentUsername = userResponse['data']['username']; } - + if (leaderboardResponse.status) { _leaderboardData = leaderboardResponse.data; } else { - _errorMessage = leaderboardResponse.message ?? 'Failed to load leaderboard'; + _errorMessage = + leaderboardResponse.message ?? 'Failed to load leaderboard'; } }); } catch (e) { @@ -402,10 +557,7 @@ class _LeaderboardContentState extends State<_LeaderboardContent> { SizedBox(height: 16), Text( 'Loading leaderboard...', - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 16, color: Colors.grey[600]), ), ], ), @@ -417,18 +569,11 @@ class _LeaderboardContentState extends State<_LeaderboardContent> { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon( - Icons.error_outline, - size: 64, - color: Colors.grey[400], - ), + Icon(Icons.error_outline, size: 64, color: Colors.grey[400]), SizedBox(height: 16), Text( _errorMessage!, - style: TextStyle( - fontSize: 16, - color: Colors.grey[600], - ), + style: TextStyle(fontSize: 16, color: Colors.grey[600]), textAlign: TextAlign.center, ), SizedBox(height: 16), @@ -467,10 +612,7 @@ class _LeaderboardContentState extends State<_LeaderboardContent> { SizedBox(height: 8), Text( 'Be the first one!', - style: TextStyle( - fontSize: 16, - color: Colors.grey[500], - ), + style: TextStyle(fontSize: 16, color: Colors.grey[500]), ), ], ), @@ -516,7 +658,9 @@ class _LeaderboardContentState extends State<_LeaderboardContent> { margin: EdgeInsets.only(bottom: 12), padding: EdgeInsets.all(16), decoration: BoxDecoration( - color: isCurrentUser ? Color(0xFF6A4C93).withOpacity(0.05) : Colors.white, + color: isCurrentUser + ? Color(0xFF6A4C93).withOpacity(0.05) + : Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( @@ -528,8 +672,8 @@ class _LeaderboardContentState extends State<_LeaderboardContent> { border: isCurrentUser ? Border.all(color: Color(0xFF6A4C93), width: 2) : rank <= 3 - ? Border.all(color: rankColor.withOpacity(0.3), width: 2) - : Border.all(color: Colors.grey[200]!, width: 1), + ? Border.all(color: rankColor.withOpacity(0.3), width: 2) + : Border.all(color: Colors.grey[200]!, width: 1), ), child: Row( children: [ @@ -558,12 +702,13 @@ class _LeaderboardContentState extends State<_LeaderboardContent> { CircleAvatar( radius: 25, backgroundColor: Color(0xFF6A4C93).withOpacity(0.1), - backgroundImage: entry.avatar != null && entry.avatar!.isNotEmpty + backgroundImage: + entry.avatar != null && entry.avatar!.isNotEmpty ? NetworkImage(entry.avatar!) : null, child: entry.avatar == null || entry.avatar!.isEmpty ? Text( - entry.displayName.isNotEmpty + entry.displayName.isNotEmpty ? entry.displayName[0].toUpperCase() : '?', style: TextStyle( @@ -594,7 +739,10 @@ class _LeaderboardContentState extends State<_LeaderboardContent> { if (isCurrentUser) ...[ SizedBox(width: 8), Container( - padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), + padding: EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), decoration: BoxDecoration( color: Color(0xFF6A4C93), borderRadius: BorderRadius.circular(8),