2025-05-29 14:08:39 +02:00

413 lines
12 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:insparkspokalen_ui/services/backend/LeaderBordSettingsService.dart';
import 'package:insparkspokalen_ui/models/teamModel.dart';
import 'package:insparkspokalen_ui/services/backend/teamService.dart';
import 'package:insparkspokalen_ui/services/googleAuthService.dart';
import 'package:insparkspokalen_ui/teams/teamImage.dart';
import 'package:provider/provider.dart';
import 'package:flutter_svg/svg.dart';
import '../services/backend/userService.dart';
class Leaderboard extends StatefulWidget {
const Leaderboard({super.key});
@override
State<Leaderboard> createState() => _LeaderboardState();
}
class _LeaderboardState extends State<Leaderboard> {
late TeamService teamService;
late UserService userService;
List<TeamModel> groups = [];
bool isLoading = true;
final SettingsService settingsService = SettingsService();
bool showLeaderboard = true;
TeamModel? userTeam;
@override
void initState() {
super.initState();
teamService = Provider.of<TeamService>(context, listen: false);
_loadGroups();
settingsService.getLeaderboardVisibility().then((value) {
setState(() {
showLeaderboard = value;
});
});
Timer.periodic(const Duration(seconds: 10), (_) {
_loadGroups();
});
}
Future<void> _loadGroups() async {
final result = await teamService.showTeams();
result.sort((a, b) => b.score.compareTo(a.score));
final currentUser = GoogleAuthService.getCurrentUser();
TeamModel? fetchedUserTeam;
if (currentUser != null) {
int? teamId = await UserService().getUserTeamByEmail(currentUser.email);
fetchedUserTeam = await teamService.getTeamById(teamId!);
}
setState(() {
groups = result;
userTeam = fetchedUserTeam;
isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (showLeaderboard)
Stack(
alignment: Alignment.topCenter,
children: [
// Halvcirkelbakgrund
// Innehållet
Padding(
padding: const EdgeInsets.only(top: 100),
child:
isLoading
? const Center(child: CircularProgressIndicator())
: _TopplistaBody(teams: groups, userTeam: userTeam),
),
],
)
else
const Padding(
padding: EdgeInsets.all(24.0),
child: Center(
child: Text(
'Leaderboard är för närvarande dold',
style: TextStyle(color: Colors.white, fontSize: 18),
),
),
),
],
),
);
}
}
class _GreyBackground extends StatelessWidget {
final Widget child;
const _GreyBackground({required this.child});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(16.0),
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: const Color(0xFFDAA520),
borderRadius: BorderRadius.circular(40),
),
child: child,
);
}
}
class _TopplistaBody extends StatelessWidget {
final List<TeamModel> teams;
final TeamModel? userTeam;
const _TopplistaBody({required this.teams, required this.userTeam});
@override
Widget build(BuildContext context) {
//if (teams.length < 3) return const SizedBox();
int? userPlace;
if (userTeam != null) {
final match = teams.indexWhere((t) => t.teamId == userTeam!.teamId);
if (match != -1) {
userPlace = match + 1;
}
}
return Column(
children: [
PodiumWidget(topTeams: teams),
const SizedBox(height: 24),
...teams
.skip(3)
.toList()
.asMap()
.entries
.map(
(entry) =>
GruppKort.GroupCard(group: entry.value, place: entry.key + 4),
),
if (userTeam != null && userPlace != null)
Padding(
padding: const EdgeInsets.only(top: 32.0),
child: Text(
'Ditt lag: ${userTeam!.name} ligger på plats $userPlace med ${userTeam!.score} poäng',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
),
],
);
}
}
class GruppKort extends StatelessWidget {
final TeamModel group;
final int place;
const GruppKort.GroupCard({
super.key,
required this.group,
required this.place,
});
/* Color _getMedalColor(int place) {
switch (place) {
case 1:
return Colors.amber;
case 2:
return Color(0xFFC0C0C0); // silver
case 3:
return Color(0xFFCD7F32); // bronze
default:
return Colors.transparent;
}
} */
@override
Widget build(BuildContext context) {
Widget leadingWidget;
if (place <= 3) {
leadingWidget = Stack(
alignment: Alignment.center,
children: [
//Icon(Icons.emoji_events, color: _getMedalColor(place), size: 40),
Text(
'$place',
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
),
),
],
);
} else {
leadingWidget = CircleAvatar(
backgroundColor: Colors.white,
radius: 30,
child:
group.imageUrl == null || group.imageUrl!.isEmpty
? Text(
'$place',
style: const TextStyle(
color: Colors.black,
fontWeight: FontWeight.bold,
),
)
: TeamImage(imageUrl: group.imageUrl, size: 30),
);
}
return Card(
elevation: 3,
margin: const EdgeInsets.symmetric(vertical: 8.0),
color: Colors.grey[900],
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ListTile(
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
leading: leadingWidget,
title: Text(
group.name,
style: const TextStyle(fontSize: 18, color: Colors.white),
),
trailing: Text(
'${group.score} poäng',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
);
}
}
class PodiumWidget extends StatelessWidget {
final List<TeamModel> topTeams;
const PodiumWidget({super.key, required this.topTeams});
@override
Widget build(BuildContext context) {
if (topTeams.isEmpty) return const SizedBox.shrink();
// Sortera och skapa en lista med exakt tre element (fyll med null om färre)
final sorted = List<TeamModel>.from(topTeams)
..sort((a, b) => b.score.compareTo(a.score));
final List<TeamModel?> podiumTeams = List.generate(
3,
(i) => i < sorted.length ? sorted[i] : null,
);
final podiumHeights = [100.0, 140.0, 100.0];
Color getPlaceColor(int place) {
switch (place) {
case 1:
return const Color(0xFF1F1F1F);
case 2:
case 3:
return const Color(0xFF262626);
default:
return Colors.grey;
}
}
return Column(
children: [
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.end,
children: List.generate(3, (index) {
final team =
index == 0
? podiumTeams[1] // plats 2
: index == 1
? podiumTeams[0] // plats 1
: podiumTeams[2]; // plats 3
final place =
index == 0
? 2
: index == 1
? 1
: 3;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Stack(
alignment: Alignment.bottomCenter,
clipBehavior: Clip.none,
children: [
CircleAvatar(
backgroundColor: Colors.white,
radius: 35,
child:
team != null
? TeamImage(imageUrl: team.imageUrl, size: 40)
: const Icon(
Icons.group_off,
color: Colors.grey,
),
),
Positioned(
bottom: -6,
child: DownArrow(color: Colors.white, size: 12),
),
],
),
const SizedBox(height: 8),
Container(
height: podiumHeights[index],
width: 100,
decoration: BoxDecoration(
color: getPlaceColor(place),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.white10),
),
padding: const EdgeInsets.all(8),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
team != null ? '${team.score} poäng' : '',
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
Text(
'$place',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(height: 4),
Text(
team != null ? team.name : '-',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}),
),
],
);
}
}
class DownArrow extends StatelessWidget {
final Color color;
final double size;
const DownArrow({required this.color, this.size = 12});
@override
Widget build(BuildContext context) {
return ClipPath(
clipper: _DownArrowClipper(),
child: Container(width: size, height: size, color: color),
);
}
}
class _DownArrowClipper extends CustomClipper<Path> {
@override
Path getClip(Size size) {
final path = Path();
path.moveTo(0, 0);
path.lineTo(size.width / 2, size.height);
path.lineTo(size.width, 0);
path.close();
return path;
}
@override
bool shouldReclip(CustomClipper<Path> oldClipper) => false;
}