feat: connect puzzles with backend, save game status in device
This commit is contained in:
parent
9b6c58269c
commit
fa3548f5bf
@ -16,4 +16,9 @@ class ApiConstants {
|
|||||||
|
|
||||||
// Post endpoints
|
// Post endpoints
|
||||||
static const String createPostEndpoint = '/posts/create';
|
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';
|
||||||
}
|
}
|
||||||
|
|||||||
193
frontend/lib/models/puzzle_models.dart
Normal file
193
frontend/lib/models/puzzle_models.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'wordle_page.dart';
|
import 'wordle_page.dart';
|
||||||
|
import '../../services/puzzle_service.dart';
|
||||||
|
import '../../services/user_service.dart';
|
||||||
|
import '../../models/puzzle_models.dart';
|
||||||
|
|
||||||
class PuzzlesPage extends StatefulWidget {
|
class PuzzlesPage extends StatefulWidget {
|
||||||
const PuzzlesPage({Key? key}) : super(key: key);
|
const PuzzlesPage({Key? key}) : super(key: key);
|
||||||
@ -10,6 +13,9 @@ class PuzzlesPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _PuzzlesPageState extends State<PuzzlesPage> {
|
class _PuzzlesPageState extends State<PuzzlesPage> {
|
||||||
String _currentView = 'main'; // main, leaderboard
|
String _currentView = 'main'; // main, leaderboard
|
||||||
|
DailyChallenge? _dailyChallenge;
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
|
||||||
void _showLeaderboardView() {
|
void _showLeaderboardView() {
|
||||||
setState(() {
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_currentView == 'leaderboard') {
|
if (_currentView == 'leaderboard') {
|
||||||
@ -90,12 +142,7 @@ class _PuzzlesPageState extends State<PuzzlesPage> {
|
|||||||
|
|
||||||
// Big gamified PLAY button
|
// Big gamified PLAY button
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: _isLoading ? null : _startGame,
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(builder: (context) => WordlePage()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 20),
|
padding: EdgeInsets.symmetric(horizontal: 40, vertical: 20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -131,15 +178,28 @@ class _PuzzlesPageState extends State<PuzzlesPage> {
|
|||||||
color: Color(0xFF6A4C93),
|
color: Color(0xFF6A4C93),
|
||||||
),
|
),
|
||||||
SizedBox(width: 12),
|
SizedBox(width: 12),
|
||||||
Text(
|
_isLoading
|
||||||
'PLAY NOW',
|
? SizedBox(
|
||||||
style: TextStyle(
|
width: 20,
|
||||||
fontSize: 24,
|
height: 20,
|
||||||
fontWeight: FontWeight.bold,
|
child: CircularProgressIndicator(
|
||||||
color: Color(0xFF6A4C93),
|
strokeWidth: 2,
|
||||||
letterSpacing: 1.5,
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
),
|
Color(0xFF6A4C93),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
_dailyChallenge?.attempted == true
|
||||||
|
? 'VIEW RESULTS'
|
||||||
|
: 'PLAY NOW',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
letterSpacing: 1.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Icon(
|
Icon(
|
||||||
Icons.arrow_forward_ios,
|
Icons.arrow_forward_ios,
|
||||||
@ -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),
|
SizedBox(height: 40),
|
||||||
|
|
||||||
// Leaderboard button
|
// Leaderboard button
|
||||||
@ -232,67 +336,150 @@ class _PuzzlesPageState extends State<PuzzlesPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wrapper class for Leaderboard content without the scaffold
|
// Wrapper class for Leaderboard content without the scaffold
|
||||||
class _LeaderboardContent extends StatelessWidget {
|
class _LeaderboardContent extends StatefulWidget {
|
||||||
final List<Map<String, dynamic>> leaderboardData = [
|
@override
|
||||||
{
|
_LeaderboardContentState createState() => _LeaderboardContentState();
|
||||||
"user": {
|
}
|
||||||
"displayName": "Ahmed Hassan",
|
|
||||||
"avatar": "https://via.placeholder.com/50",
|
class _LeaderboardContentState extends State<_LeaderboardContent> {
|
||||||
},
|
List<LeaderboardEntry>? _leaderboardData;
|
||||||
"attempts": 3,
|
bool _isLoading = true;
|
||||||
"time": "2025-01-15T08:45:23.000Z",
|
String? _errorMessage;
|
||||||
},
|
String? _currentUsername;
|
||||||
{
|
|
||||||
"user": {
|
@override
|
||||||
"displayName": "Sarah Abdullah",
|
void initState() {
|
||||||
"avatar": "https://via.placeholder.com/50",
|
super.initState();
|
||||||
},
|
_loadLeaderboard();
|
||||||
"attempts": 4,
|
}
|
||||||
"time": "2025-01-15T09:12:45.000Z",
|
|
||||||
},
|
Future<void> _loadLeaderboard() async {
|
||||||
{
|
setState(() {
|
||||||
"user": {
|
_isLoading = true;
|
||||||
"displayName": "Omar Al-Rashid",
|
_errorMessage = null;
|
||||||
"avatar": "https://via.placeholder.com/50",
|
});
|
||||||
},
|
|
||||||
"attempts": 2,
|
try {
|
||||||
"time": "2025-01-15T08:23:12.000Z",
|
// Get current user and leaderboard data in parallel
|
||||||
},
|
final results = await Future.wait([
|
||||||
{
|
UserService.getCurrentUser(),
|
||||||
"user": {
|
PuzzleService.getLeaderboard(),
|
||||||
"displayName": "Fatima Al-Zahra",
|
]);
|
||||||
"avatar": "https://via.placeholder.com/50",
|
|
||||||
},
|
final userResponse = results[0] as Map<String, dynamic>;
|
||||||
"attempts": 5,
|
final leaderboardResponse = results[1] as LeaderboardResponse;
|
||||||
"time": "2025-01-15T09:34:56.000Z",
|
|
||||||
},
|
setState(() {
|
||||||
{
|
_isLoading = false;
|
||||||
"user": {
|
if (userResponse['success'] == true && userResponse['data'] != null) {
|
||||||
"displayName": "Khalid Mohammed",
|
_currentUsername = userResponse['data']['username'];
|
||||||
"avatar": "https://via.placeholder.com/50",
|
}
|
||||||
},
|
|
||||||
"attempts": 3,
|
if (leaderboardResponse.status) {
|
||||||
"time": "2025-01-15T08:56:34.000Z",
|
_leaderboardData = leaderboardResponse.data;
|
||||||
},
|
} else {
|
||||||
{
|
_errorMessage = leaderboardResponse.message ?? 'Failed to load leaderboard';
|
||||||
"user": {
|
}
|
||||||
"displayName": "Layla Ibrahim",
|
});
|
||||||
"avatar": "https://via.placeholder.com/50",
|
} catch (e) {
|
||||||
},
|
setState(() {
|
||||||
"attempts": 4,
|
_isLoading = false;
|
||||||
"time": "2025-01-15T09:18:27.000Z",
|
_errorMessage = 'Error loading leaderboard';
|
||||||
},
|
});
|
||||||
];
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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)
|
// Sort leaderboard by time (fastest first)
|
||||||
final sortedData = List<Map<String, dynamic>>.from(leaderboardData);
|
final sortedData = List<LeaderboardEntry>.from(_leaderboardData!);
|
||||||
sortedData.sort((a, b) {
|
sortedData.sort((a, b) => a.submittedAt.compareTo(b.submittedAt));
|
||||||
final timeA = DateTime.parse(a['time']);
|
|
||||||
final timeB = DateTime.parse(b['time']);
|
|
||||||
return timeA.compareTo(timeB);
|
|
||||||
});
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@ -303,13 +490,11 @@ class _LeaderboardContent extends StatelessWidget {
|
|||||||
itemCount: sortedData.length,
|
itemCount: sortedData.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final entry = sortedData[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 rank = index + 1;
|
||||||
|
final isCurrentUser = entry.username == _currentUsername;
|
||||||
|
|
||||||
// Format time display
|
// Format time display
|
||||||
|
final time = entry.submittedAt;
|
||||||
final timeFormatted =
|
final timeFormatted =
|
||||||
'${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}:${time.second.toString().padLeft(2, '0')}';
|
'${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),
|
margin: EdgeInsets.only(bottom: 12),
|
||||||
padding: EdgeInsets.all(16),
|
padding: EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: isCurrentUser ? Color(0xFF6A4C93).withOpacity(0.05) : Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@ -340,9 +525,11 @@ class _LeaderboardContent extends StatelessWidget {
|
|||||||
offset: Offset(0, 2),
|
offset: Offset(0, 2),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
border: rank <= 3
|
border: isCurrentUser
|
||||||
? Border.all(color: rankColor.withOpacity(0.3), width: 2)
|
? Border.all(color: Color(0xFF6A4C93), width: 2)
|
||||||
: Border.all(color: Colors.grey[200]!, width: 1),
|
: rank <= 3
|
||||||
|
? Border.all(color: rankColor.withOpacity(0.3), width: 2)
|
||||||
|
: Border.all(color: Colors.grey[200]!, width: 1),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@ -371,14 +558,21 @@ class _LeaderboardContent extends StatelessWidget {
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 25,
|
radius: 25,
|
||||||
backgroundColor: Color(0xFF6A4C93).withOpacity(0.1),
|
backgroundColor: Color(0xFF6A4C93).withOpacity(0.1),
|
||||||
child: Text(
|
backgroundImage: entry.avatar != null && entry.avatar!.isNotEmpty
|
||||||
user['displayName'][0].toUpperCase(),
|
? NetworkImage(entry.avatar!)
|
||||||
style: TextStyle(
|
: null,
|
||||||
fontSize: 20,
|
child: entry.avatar == null || entry.avatar!.isEmpty
|
||||||
fontWeight: FontWeight.bold,
|
? Text(
|
||||||
color: Color(0xFF6A4C93),
|
entry.displayName.isNotEmpty
|
||||||
),
|
? entry.displayName[0].toUpperCase()
|
||||||
),
|
: '?',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF6A4C93),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
SizedBox(width: 16),
|
SizedBox(width: 16),
|
||||||
|
|
||||||
@ -387,13 +581,35 @@ class _LeaderboardContent extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Row(
|
||||||
user['displayName'],
|
children: [
|
||||||
style: TextStyle(
|
Text(
|
||||||
fontSize: 16,
|
entry.displayName,
|
||||||
fontWeight: FontWeight.w600,
|
style: TextStyle(
|
||||||
color: Colors.black87,
|
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),
|
SizedBox(height: 4),
|
||||||
Row(
|
Row(
|
||||||
@ -419,7 +635,7 @@ class _LeaderboardContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
SizedBox(width: 4),
|
SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'$attempts attempts',
|
'${entry.attempts} attempts',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Colors.grey[600],
|
color: Colors.grey[600],
|
||||||
|
|||||||
@ -2,16 +2,25 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import '../../services/puzzle_service.dart';
|
||||||
|
import '../../models/puzzle_models.dart';
|
||||||
|
|
||||||
class WordlePage extends StatefulWidget {
|
class WordlePage extends StatefulWidget {
|
||||||
|
final String? targetWord;
|
||||||
|
final DailyChallenge? dailyChallenge;
|
||||||
|
|
||||||
|
const WordlePage({Key? key, this.targetWord, this.dailyChallenge})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_WordlePageState createState() => _WordlePageState();
|
_WordlePageState createState() => _WordlePageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
|
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 MAX_ATTEMPTS = 6;
|
||||||
static const int WORD_LENGTH = 5;
|
static const int WORD_LENGTH = 5;
|
||||||
|
DateTime? gameStartTime;
|
||||||
|
|
||||||
List<List<String>> grid = List.generate(
|
List<List<String>> grid = List.generate(
|
||||||
MAX_ATTEMPTS,
|
MAX_ATTEMPTS,
|
||||||
@ -37,9 +46,12 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
TARGET_WORD = widget.targetWord ?? "HELLO";
|
||||||
|
gameStartTime = DateTime.now();
|
||||||
_initializeAnimations();
|
_initializeAnimations();
|
||||||
_initializeKeyboardStates();
|
_initializeKeyboardStates();
|
||||||
_focusNode = FocusNode();
|
_focusNode = FocusNode();
|
||||||
|
_loadGameState();
|
||||||
|
|
||||||
// Request focus for keyboard input on web/desktop
|
// Request focus for keyboard input on web/desktop
|
||||||
if (kIsWeb || (!kIsWeb && !Platform.isIOS && !Platform.isAndroid)) {
|
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() {
|
void _initializeAnimations() {
|
||||||
_revealController = AnimationController(
|
_revealController = AnimationController(
|
||||||
duration: Duration(milliseconds: 1500),
|
duration: Duration(milliseconds: 1500),
|
||||||
@ -92,6 +161,7 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
|
|||||||
grid[currentRow][currentCol] = letter;
|
grid[currentRow][currentCol] = letter;
|
||||||
currentCol++;
|
currentCol++;
|
||||||
});
|
});
|
||||||
|
_saveGameState();
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -102,6 +172,7 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
|
|||||||
currentCol--;
|
currentCol--;
|
||||||
grid[currentRow][currentCol] = '';
|
grid[currentRow][currentCol] = '';
|
||||||
});
|
});
|
||||||
|
_saveGameState();
|
||||||
HapticFeedback.lightImpact();
|
HapticFeedback.lightImpact();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -131,19 +202,24 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
|
|||||||
setState(() {
|
setState(() {
|
||||||
gameWon = true;
|
gameWon = true;
|
||||||
});
|
});
|
||||||
|
await _saveGameState();
|
||||||
|
await _submitAttempt(currentRow + 1, true);
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
_showGameEndDialog(true);
|
_showCompletedGameUI();
|
||||||
} else if (currentRow == MAX_ATTEMPTS - 1) {
|
} else if (currentRow == MAX_ATTEMPTS - 1) {
|
||||||
setState(() {
|
setState(() {
|
||||||
gameLost = true;
|
gameLost = true;
|
||||||
});
|
});
|
||||||
|
await _saveGameState();
|
||||||
|
await _submitAttempt(MAX_ATTEMPTS, false);
|
||||||
HapticFeedback.heavyImpact();
|
HapticFeedback.heavyImpact();
|
||||||
_showGameEndDialog(false);
|
_showCompletedGameUI();
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
currentRow++;
|
currentRow++;
|
||||||
currentCol = 0;
|
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) {
|
void _showGameEndDialog(bool won) {
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
@ -416,8 +511,13 @@ class _WordlePageState extends State<WordlePage> with TickerProviderStateMixin {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Keyboard
|
// Keyboard or completion message
|
||||||
Container(padding: EdgeInsets.all(8), child: _buildKeyboard()),
|
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() {
|
Widget _buildKeyboard() {
|
||||||
List<List<String>> keyboardLayout = [
|
List<List<String>> keyboardLayout = [
|
||||||
['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
|
['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'],
|
||||||
|
|||||||
139
frontend/lib/services/puzzle_service.dart
Normal file
139
frontend/lib/services/puzzle_service.dart
Normal 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}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user