feat: connect puzzles with backend, save game status in device

This commit is contained in:
sBubshait 2025-08-06 13:03:48 +03:00
parent 9b6c58269c
commit fa3548f5bf
5 changed files with 839 additions and 101 deletions

View File

@ -16,4 +16,9 @@ class ApiConstants {
// Post endpoints
static const String createPostEndpoint = '/posts/create';
// Puzzle endpoints
static const String dailyChallengeEndpoint = '/puzzles/dailyChallenge';
static const String puzzleAttemptEndpoint = '/puzzles/attempt';
static const String puzzleLeaderboardEndpoint = '/puzzles/leaderboard';
}

View File

@ -0,0 +1,193 @@
class DailyChallenge {
final String wordle;
final bool attempted;
final bool solved;
final int? attempts;
final DateTime? solveTime;
DailyChallenge({
required this.wordle,
required this.attempted,
required this.solved,
this.attempts,
this.solveTime,
});
factory DailyChallenge.fromJson(Map<String, dynamic> json) {
return DailyChallenge(
wordle: json['wordle'] ?? '',
attempted: json['attempted'] ?? false,
solved: json['solved'] ?? false,
attempts: json['attempts'],
solveTime: json['solveTime'] != null
? DateTime.parse(json['solveTime'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'wordle': wordle,
'attempted': attempted,
'solved': solved,
'attempts': attempts,
'solveTime': solveTime?.toIso8601String(),
};
}
}
class DailyChallengeResponse {
final bool status;
final String? message;
final DailyChallenge? data;
DailyChallengeResponse({
required this.status,
this.message,
this.data,
});
factory DailyChallengeResponse.fromJson(Map<String, dynamic> json) {
return DailyChallengeResponse(
status: json['status'] ?? false,
message: json['message'],
data: json['data'] != null
? DailyChallenge.fromJson(json['data'])
: null,
);
}
}
class PuzzleAttempt {
final int attempts;
final bool solved;
PuzzleAttempt({
required this.attempts,
required this.solved,
});
Map<String, dynamic> toJson() {
return {
'attempts': attempts,
'solved': solved,
};
}
}
class GameState {
final String targetWord;
final List<String> guesses;
final int currentAttempt;
final bool isGameComplete;
final bool isWon;
final DateTime? solveTime;
final String gameDate;
GameState({
required this.targetWord,
required this.guesses,
required this.currentAttempt,
required this.isGameComplete,
required this.isWon,
this.solveTime,
required this.gameDate,
});
factory GameState.fromJson(Map<String, dynamic> json) {
return GameState(
targetWord: json['targetWord'] ?? '',
guesses: List<String>.from(json['guesses'] ?? []),
currentAttempt: json['currentAttempt'] ?? 0,
isGameComplete: json['isGameComplete'] ?? false,
isWon: json['isWon'] ?? false,
solveTime: json['solveTime'] != null
? DateTime.parse(json['solveTime'])
: null,
gameDate: json['gameDate'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'targetWord': targetWord,
'guesses': guesses,
'currentAttempt': currentAttempt,
'isGameComplete': isGameComplete,
'isWon': isWon,
'solveTime': solveTime?.toIso8601String(),
'gameDate': gameDate,
};
}
GameState copyWith({
String? targetWord,
List<String>? guesses,
int? currentAttempt,
bool? isGameComplete,
bool? isWon,
DateTime? solveTime,
String? gameDate,
}) {
return GameState(
targetWord: targetWord ?? this.targetWord,
guesses: guesses ?? this.guesses,
currentAttempt: currentAttempt ?? this.currentAttempt,
isGameComplete: isGameComplete ?? this.isGameComplete,
isWon: isWon ?? this.isWon,
solveTime: solveTime ?? this.solveTime,
gameDate: gameDate ?? this.gameDate,
);
}
}
class LeaderboardEntry {
final String displayName;
final String username;
final String? avatar;
final int attempts;
final bool solved;
final DateTime submittedAt;
LeaderboardEntry({
required this.displayName,
required this.username,
this.avatar,
required this.attempts,
required this.solved,
required this.submittedAt,
});
factory LeaderboardEntry.fromJson(Map<String, dynamic> json) {
return LeaderboardEntry(
displayName: json['displayName'] ?? '',
username: json['username'] ?? '',
avatar: json['avatar'],
attempts: json['attempts'] ?? 0,
solved: json['solved'] ?? false,
submittedAt: DateTime.parse(json['submittedAt']),
);
}
}
class LeaderboardResponse {
final bool status;
final String? message;
final List<LeaderboardEntry>? data;
LeaderboardResponse({
required this.status,
this.message,
this.data,
});
factory LeaderboardResponse.fromJson(Map<String, dynamic> json) {
return LeaderboardResponse(
status: json['status'] ?? false,
message: json['message'],
data: json['data'] != null
? (json['data'] as List).map((e) => LeaderboardEntry.fromJson(e)).toList()
: null,
);
}
}

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'wordle_page.dart';
import '../../services/puzzle_service.dart';
import '../../services/user_service.dart';
import '../../models/puzzle_models.dart';
class PuzzlesPage extends StatefulWidget {
const PuzzlesPage({Key? key}) : super(key: key);
@ -10,6 +13,9 @@ class PuzzlesPage extends StatefulWidget {
class _PuzzlesPageState extends State<PuzzlesPage> {
String _currentView = 'main'; // main, leaderboard
DailyChallenge? _dailyChallenge;
bool _isLoading = false;
String? _errorMessage;
void _showLeaderboardView() {
setState(() {
@ -23,6 +29,52 @@ class _PuzzlesPageState extends State<PuzzlesPage> {
});
}
@override
void initState() {
super.initState();
_loadDailyChallenge();
}
Future<void> _loadDailyChallenge() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final challenge = await PuzzleService.getDailyChallenge();
setState(() {
_dailyChallenge = challenge;
_isLoading = false;
});
} catch (e) {
setState(() {
_errorMessage = 'Failed to load daily challenge';
_isLoading = false;
});
}
}
void _startGame() async {
if (_dailyChallenge == null) {
_loadDailyChallenge();
return;
}
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WordlePage(
targetWord: _dailyChallenge!.wordle,
dailyChallenge: _dailyChallenge!,
),
),
).then((_) {
// Refresh daily challenge when returning from game
_loadDailyChallenge();
});
}
@override
Widget build(BuildContext context) {
if (_currentView == 'leaderboard') {
@ -90,12 +142,7 @@ class _PuzzlesPageState extends State<PuzzlesPage> {
// Big gamified PLAY button
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => WordlePage()),
);
},
onTap: _isLoading ? null : _startGame,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 20),
decoration: BoxDecoration(
@ -131,8 +178,21 @@ class _PuzzlesPageState extends State<PuzzlesPage> {
color: Color(0xFF6A4C93),
),
SizedBox(width: 12),
Text(
'PLAY NOW',
_isLoading
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Color(0xFF6A4C93),
),
),
)
: Text(
_dailyChallenge?.attempted == true
? 'VIEW RESULTS'
: 'PLAY NOW',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
@ -150,6 +210,50 @@ class _PuzzlesPageState extends State<PuzzlesPage> {
),
),
),
// Show error message if any
if (_errorMessage != null) ...[
SizedBox(height: 20),
Container(
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
decoration: BoxDecoration(
color: Colors.red.withOpacity(0.2),
borderRadius: BorderRadius.circular(20),
),
child: Text(
_errorMessage!,
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
],
// 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),
borderRadius: BorderRadius.circular(20),
),
child: Text(
_dailyChallenge!.solved
? 'You solved this at ${PuzzleService.formatSolveTime(_dailyChallenge!.solveTime!)} in ${_dailyChallenge!.attempts} attempts!'
: 'You attempted this challenge but didn\'t solve it in ${_dailyChallenge!.attempts} attempts',
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
),
],
SizedBox(height: 40),
// Leaderboard button
@ -232,67 +336,150 @@ class _PuzzlesPageState extends State<PuzzlesPage> {
}
// Wrapper class for Leaderboard content without the scaffold
class _LeaderboardContent extends StatelessWidget {
final List<Map<String, dynamic>> 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",
},
];
class _LeaderboardContent extends StatefulWidget {
@override
_LeaderboardContentState createState() => _LeaderboardContentState();
}
class _LeaderboardContentState extends State<_LeaderboardContent> {
List<LeaderboardEntry>? _leaderboardData;
bool _isLoading = true;
String? _errorMessage;
String? _currentUsername;
@override
void initState() {
super.initState();
_loadLeaderboard();
}
Future<void> _loadLeaderboard() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
// Get current user and leaderboard data in parallel
final results = await Future.wait([
UserService.getCurrentUser(),
PuzzleService.getLeaderboard(),
]);
final userResponse = results[0] as Map<String, dynamic>;
final leaderboardResponse = results[1] as LeaderboardResponse;
setState(() {
_isLoading = false;
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';
}
});
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = 'Error loading leaderboard';
});
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFF6A4C93)),
),
SizedBox(height: 16),
Text(
'Loading leaderboard...',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
],
),
);
}
if (_errorMessage != null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
_errorMessage!,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
SizedBox(height: 16),
ElevatedButton(
onPressed: _loadLeaderboard,
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
),
child: Text('Try Again'),
),
],
),
);
}
if (_leaderboardData == null || _leaderboardData!.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.emoji_events_outlined,
size: 64,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
'No one solved the puzzle today.',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: Colors.grey[600],
),
),
SizedBox(height: 8),
Text(
'Be the first one!',
style: TextStyle(
fontSize: 16,
color: Colors.grey[500],
),
),
],
),
);
}
// Sort leaderboard by time (fastest first)
final sortedData = List<Map<String, dynamic>>.from(leaderboardData);
sortedData.sort((a, b) {
final timeA = DateTime.parse(a['time']);
final timeB = DateTime.parse(b['time']);
return timeA.compareTo(timeB);
});
final sortedData = List<LeaderboardEntry>.from(_leaderboardData!);
sortedData.sort((a, b) => a.submittedAt.compareTo(b.submittedAt));
return Column(
children: [
@ -303,13 +490,11 @@ class _LeaderboardContent extends StatelessWidget {
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;
final isCurrentUser = entry.username == _currentUsername;
// Format time display
final time = entry.submittedAt;
final timeFormatted =
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}';
@ -331,7 +516,7 @@ class _LeaderboardContent extends StatelessWidget {
margin: EdgeInsets.only(bottom: 12),
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
color: isCurrentUser ? Color(0xFF6A4C93).withOpacity(0.05) : Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
@ -340,7 +525,9 @@ class _LeaderboardContent extends StatelessWidget {
offset: Offset(0, 2),
),
],
border: rank <= 3
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),
),
@ -371,14 +558,21 @@ class _LeaderboardContent extends StatelessWidget {
CircleAvatar(
radius: 25,
backgroundColor: Color(0xFF6A4C93).withOpacity(0.1),
child: Text(
user['displayName'][0].toUpperCase(),
backgroundImage: entry.avatar != null && entry.avatar!.isNotEmpty
? NetworkImage(entry.avatar!)
: null,
child: entry.avatar == null || entry.avatar!.isEmpty
? Text(
entry.displayName.isNotEmpty
? entry.displayName[0].toUpperCase()
: '?',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF6A4C93),
),
),
)
: null,
),
SizedBox(width: 16),
@ -386,15 +580,37 @@ class _LeaderboardContent extends StatelessWidget {
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
user['displayName'],
entry.displayName,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
if (isCurrentUser) ...[
SizedBox(width: 8),
Container(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Color(0xFF6A4C93),
borderRadius: BorderRadius.circular(8),
),
child: Text(
'YOU',
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
],
),
SizedBox(height: 4),
Row(
children: [
@ -419,7 +635,7 @@ class _LeaderboardContent extends StatelessWidget {
),
SizedBox(width: 4),
Text(
'$attempts attempts',
'${entry.attempts} attempts',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],

View File

@ -2,16 +2,25 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'dart:io';
import '../../services/puzzle_service.dart';
import '../../models/puzzle_models.dart';
class WordlePage extends StatefulWidget {
final String? targetWord;
final DailyChallenge? dailyChallenge;
const WordlePage({Key? key, this.targetWord, this.dailyChallenge})
: super(key: key);
@override
_WordlePageState createState() => _WordlePageState();
}
class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
static const String TARGET_WORD = "HELLO";
late String TARGET_WORD;
static const int MAX_ATTEMPTS = 6;
static const int WORD_LENGTH = 5;
DateTime? gameStartTime;
List<List<String>> grid = List.generate(
MAX_ATTEMPTS,
@ -37,9 +46,12 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
@override
void initState() {
super.initState();
TARGET_WORD = widget.targetWord ?? "HELLO";
gameStartTime = DateTime.now();
_initializeAnimations();
_initializeKeyboardStates();
_focusNode = FocusNode();
_loadGameState();
// Request focus for keyboard input on web/desktop
if (kIsWeb || (!kIsWeb && !Platform.isIOS && !Platform.isAndroid)) {
@ -49,6 +61,63 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
}
}
Future<void> _loadGameState() async {
final gameState = await PuzzleService.loadGameState();
if (gameState != null && gameState.targetWord == TARGET_WORD) {
setState(() {
// Restore game state
for (int i = 0; i < gameState.guesses.length; i++) {
if (i < MAX_ATTEMPTS && gameState.guesses[i].isNotEmpty) {
for (int j = 0; j < WORD_LENGTH; j++) {
if (j < gameState.guesses[i].length) {
grid[i][j] = gameState.guesses[i][j];
}
}
// Evaluate the guess to set proper states
List<LetterState> states = _evaluateGuess(gameState.guesses[i]);
gridStates[i] = states;
_updateKeyboardStates(gameState.guesses[i], states);
}
}
currentRow = gameState.currentAttempt;
currentCol = 0;
gameWon = gameState.isWon;
gameLost = gameState.isGameComplete && !gameState.isWon;
if (gameState.solveTime != null) {
gameStartTime = gameState.solveTime;
}
});
// If game is already completed, show the completed state
if (gameState.isGameComplete) {
WidgetsBinding.instance.addPostFrameCallback((_) {
_showCompletedGameUI();
});
}
}
}
Future<void> _saveGameState() async {
final guesses = <String>[];
for (int i = 0; i <= currentRow && i < MAX_ATTEMPTS; i++) {
if (grid[i].any((cell) => cell.isNotEmpty)) {
guesses.add(grid[i].join(''));
}
}
final gameState = GameState(
targetWord: TARGET_WORD,
guesses: guesses,
currentAttempt: currentRow,
isGameComplete: gameWon || gameLost,
isWon: gameWon,
solveTime: gameWon ? DateTime.now() : null,
gameDate: PuzzleService.getTodayDateString(),
);
await PuzzleService.saveGameState(gameState);
}
void _initializeAnimations() {
_revealController = AnimationController(
duration: Duration(milliseconds: 1500),
@ -92,6 +161,7 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
grid[currentRow][currentCol] = letter;
currentCol++;
});
_saveGameState();
HapticFeedback.lightImpact();
}
}
@ -102,6 +172,7 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
currentCol--;
grid[currentRow][currentCol] = '';
});
_saveGameState();
HapticFeedback.lightImpact();
}
}
@ -131,19 +202,24 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
setState(() {
gameWon = true;
});
await _saveGameState();
await _submitAttempt(currentRow + 1, true);
HapticFeedback.heavyImpact();
_showGameEndDialog(true);
_showCompletedGameUI();
} else if (currentRow == MAX_ATTEMPTS - 1) {
setState(() {
gameLost = true;
});
await _saveGameState();
await _submitAttempt(MAX_ATTEMPTS, false);
HapticFeedback.heavyImpact();
_showGameEndDialog(false);
_showCompletedGameUI();
} else {
setState(() {
currentRow++;
currentCol = 0;
});
await _saveGameState();
}
}
}
@ -191,6 +267,25 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
}
}
Future<void> _submitAttempt(int attempts, bool solved) async {
try {
await PuzzleService.submitAttempt(attempts: attempts, solved: solved);
} catch (e) {
print('Failed to submit attempt: $e');
}
}
void _showCompletedGameUI() {
// Remove the old dialog and show a new completed game state
// This will be handled in the UI by showing a different keyboard/message area
}
void _goToLeaderboard() {
Navigator.pop(
context,
); // Go back to puzzles page which will show leaderboard option
}
void _showGameEndDialog(bool won) {
showDialog(
context: context,
@ -416,8 +511,13 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
),
),
),
// Keyboard
Container(padding: EdgeInsets.all(8), child: _buildKeyboard()),
// Keyboard or completion message
Container(
padding: EdgeInsets.all(8),
child: (gameWon || gameLost)
? _buildCompletionMessage()
: _buildKeyboard(),
),
],
),
),
@ -426,6 +526,91 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
);
}
Widget _buildCompletionMessage() {
String message;
String buttonText = 'Go to Leaderboard';
Color messageColor;
IconData messageIcon;
if (gameWon) {
if (widget.dailyChallenge?.solved == true &&
widget.dailyChallenge?.solveTime != null) {
// Already completed today
message =
'You solved this at ${PuzzleService.formatSolveTime(widget.dailyChallenge!.solveTime!)} in ${widget.dailyChallenge!.attempts} attempts!';
} else {
// Just completed now
message = 'You got it!\nSolved in ${currentRow + 1} attempts!';
}
messageColor = Colors.green;
messageIcon = Icons.celebration;
} else {
if (widget.dailyChallenge?.attempted == true) {
// Already attempted today
message =
'You attempted this challenge but didn\'t solve it in ${widget.dailyChallenge!.attempts} attempts.\nThe word was: $TARGET_WORD';
} else {
// Just failed now
message = 'Better luck next time!\nThe word was: $TARGET_WORD';
}
messageColor = Colors.orange;
messageIcon = Icons.sentiment_neutral;
}
return Column(
children: [
Container(
padding: EdgeInsets.all(24),
margin: EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: messageColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: messageColor.withOpacity(0.3), width: 2),
),
child: Column(
children: [
Icon(messageIcon, color: messageColor, size: 48),
SizedBox(height: 16),
Text(
message,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
textAlign: TextAlign.center,
),
],
),
),
SizedBox(height: 24),
ElevatedButton(
onPressed: _goToLeaderboard,
style: ElevatedButton.styleFrom(
backgroundColor: Color(0xFF6A4C93),
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(horizontal: 32, vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
elevation: 4,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.leaderboard, size: 20),
SizedBox(width: 8),
Text(
buttonText,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
],
),
),
],
);
}
Widget _buildKeyboard() {
List<List<String>> keyboardLayout = [
['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],

View File

@ -0,0 +1,139 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'http_service.dart';
import '../constants/api_constants.dart';
import '../models/puzzle_models.dart';
class PuzzleService {
static const FlutterSecureStorage _storage = FlutterSecureStorage();
static const String _gameStateKey = 'wordle_game_state';
static Future<DailyChallenge?> getDailyChallenge() async {
try {
final response = await HttpService.get(ApiConstants.dailyChallengeEndpoint);
if (response.statusCode == 200) {
final dailyChallengeResponse = DailyChallengeResponse.fromJson(
jsonDecode(response.body)
);
return dailyChallengeResponse.data;
} else {
print('Failed to get daily challenge: ${response.statusCode}');
return null;
}
} catch (e) {
print('Error getting daily challenge: $e');
return null;
}
}
static Future<bool> submitAttempt({
required int attempts,
required bool solved,
}) async {
try {
final attempt = PuzzleAttempt(attempts: attempts, solved: solved);
final response = await HttpService.post(
ApiConstants.puzzleAttemptEndpoint,
attempt.toJson(),
);
if (response.statusCode == 200 || response.statusCode == 201) {
final responseData = jsonDecode(response.body);
return responseData['status'] ?? false;
} else {
print('Failed to submit attempt: ${response.statusCode}');
return false;
}
} catch (e) {
print('Error submitting attempt: $e');
return false;
}
}
static Future<void> saveGameState(GameState gameState) async {
try {
await _storage.write(
key: _gameStateKey,
value: jsonEncode(gameState.toJson()),
);
} catch (e) {
print('Error saving game state: $e');
}
}
static Future<GameState?> loadGameState() async {
try {
final gameStateString = await _storage.read(key: _gameStateKey);
if (gameStateString != null) {
final gameState = GameState.fromJson(jsonDecode(gameStateString));
// Check if it's the same day - if not, return null to start fresh
final today = DateTime.now().toIso8601String().substring(0, 10);
if (gameState.gameDate != today) {
await clearGameState();
return null;
}
return gameState;
}
return null;
} catch (e) {
print('Error loading game state: $e');
return null;
}
}
static Future<void> clearGameState() async {
try {
await _storage.delete(key: _gameStateKey);
} catch (e) {
print('Error clearing game state: $e');
}
}
static String getTodayDateString() {
return DateTime.now().toIso8601String().substring(0, 10);
}
static Future<LeaderboardResponse> getLeaderboard() async {
try {
final response = await HttpService.get(ApiConstants.puzzleLeaderboardEndpoint);
if (response.statusCode == 200) {
return LeaderboardResponse.fromJson(jsonDecode(response.body));
} else {
print('Failed to get leaderboard: ${response.statusCode}');
return LeaderboardResponse(
status: false,
message: 'Failed to load leaderboard',
data: null,
);
}
} catch (e) {
print('Error getting leaderboard: $e');
return LeaderboardResponse(
status: false,
message: 'Error loading leaderboard',
data: null,
);
}
}
static String formatSolveTime(DateTime solveTime) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final solveDate = DateTime(solveTime.year, solveTime.month, solveTime.day);
if (solveDate == today) {
// Format as time today (e.g., "2:36 PM")
final hour = solveTime.hour > 12 ? solveTime.hour - 12 : solveTime.hour == 0 ? 12 : solveTime.hour;
final minute = solveTime.minute.toString().padLeft(2, '0');
final period = solveTime.hour >= 12 ? 'PM' : 'AM';
return '$hour:$minute $period today';
} else {
// Format as full date if not today
return '${solveTime.day}/${solveTime.month}/${solveTime.year}';
}
}
}