Commented Code

main
joschy2002 2025-06-25 18:20:06 +02:00
parent 8c0d3676e9
commit 9a8561e45e
13 changed files with 254 additions and 58 deletions

View File

@ -1,5 +1,7 @@
// main.dart // main.dart
// Einstiegspunkt der App und globale Konfigurationen // Entry point of the app and global configuration.
// This file initializes Firebase, sets up localization, and launches the main app widget.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:trainerbox/firebase_options.dart'; import 'package:trainerbox/firebase_options.dart';
@ -8,15 +10,21 @@ import 'screens/login_screen.dart';
import 'screens/search_tab.dart'; import 'screens/search_tab.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
/// Main entry point for the Flutter application.
void main() async { void main() async {
// Ensures that widget binding is initialized before using platform channels.
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase with platform-specific options.
await Firebase.initializeApp( await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, options: DefaultFirebaseOptions.currentPlatform,
); );
// Initialize date formatting for German locale.
await initializeDateFormatting('de_DE', null); await initializeDateFormatting('de_DE', null);
// Start the app.
runApp(const MyApp()); runApp(const MyApp());
} }
/// The root widget of the application.
class MyApp extends StatefulWidget { class MyApp extends StatefulWidget {
const MyApp({super.key}); const MyApp({super.key});
@ -24,15 +32,18 @@ class MyApp extends StatefulWidget {
State<MyApp> createState() => _MyAppState(); State<MyApp> createState() => _MyAppState();
} }
/// State for the main app widget, manages login state and routing.
class _MyAppState extends State<MyApp> { class _MyAppState extends State<MyApp> {
bool _loggedIn = false; bool _loggedIn = false;
/// Called when login is successful.
void _handleLoginSuccess() { void _handleLoginSuccess() {
setState(() { setState(() {
_loggedIn = true; _loggedIn = true;
}); });
} }
/// Called when logout is successful.
void _handleLogoutSuccess() { void _handleLogoutSuccess() {
setState(() { setState(() {
_loggedIn = false; _loggedIn = false;
@ -48,9 +59,11 @@ class _MyAppState extends State<MyApp> {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue), colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
useMaterial3: true, useMaterial3: true,
), ),
// Show HomeScreen if logged in, otherwise show LoginScreen.
home: _loggedIn home: _loggedIn
? HomeScreen(onLogoutSuccess: _handleLogoutSuccess) ? HomeScreen(onLogoutSuccess: _handleLogoutSuccess)
: LoginScreen(onLoginSuccess: _handleLoginSuccess), : LoginScreen(onLoginSuccess: _handleLoginSuccess),
// Define named routes for navigation.
routes: { routes: {
'/search': (context) => SearchTab( '/search': (context) => SearchTab(
selectMode: (ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?)?['selectMode'] ?? false, selectMode: (ModalRoute.of(context)?.settings.arguments as Map<String, dynamic>?)?['selectMode'] ?? false,

View File

@ -1,13 +1,24 @@
// models/exercise.dart // models/exercise.dart
// Data model for a training exercise
// Datenmodell für eine Trainingsübung // Datenmodell für eine Trainingsübung
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// Represents a training exercise with a title, category, and icon.
/// Stellt eine Trainingsübung mit Titel, Kategorie und Icon dar.
class Exercise { class Exercise {
final String title; // Name der Übung /// The name/title of the exercise.
final String category; // Kategorie der Übung // Name der Übung
final IconData icon; // Icon zur Darstellung final String title;
/// The category of the exercise.
// Kategorie der Übung
final String category;
/// The icon used to visually represent the exercise.
// Icon zur Darstellung
final IconData icon;
/// Creates an Exercise instance.
/// Erstellt eine neue Instanz einer Übung.
Exercise({ Exercise({
required this.title, required this.title,
required this.category, required this.category,

View File

@ -1,3 +1,6 @@
// calendar_tab.dart
// This file contains the CalendarTab widget, which displays a calendar view of trainings and allows users to view, add, and manage training events.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart'; import 'package:table_calendar/table_calendar.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
@ -5,6 +8,7 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'training_detail_screen.dart'; import 'training_detail_screen.dart';
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
/// Mapping of training categories to colors for calendar display.
const Map<String, Color> categoryColors = { const Map<String, Color> categoryColors = {
'Aufwärmen & Mobilisation': Colors.deepOrange, 'Aufwärmen & Mobilisation': Colors.deepOrange,
'Wurf- & Torabschluss': Colors.orange, 'Wurf- & Torabschluss': Colors.orange,
@ -14,6 +18,7 @@ const Map<String, Color> categoryColors = {
'Koordination': Colors.teal, 'Koordination': Colors.teal,
}; };
/// The CalendarTab displays a calendar with all training events for the user.
class CalendarTab extends StatefulWidget { class CalendarTab extends StatefulWidget {
final DateTime? initialDate; final DateTime? initialDate;
const CalendarTab({super.key, this.initialDate}); const CalendarTab({super.key, this.initialDate});
@ -22,6 +27,7 @@ class CalendarTab extends StatefulWidget {
State<CalendarTab> createState() => _CalendarTabState(); State<CalendarTab> createState() => _CalendarTabState();
} }
/// State for the CalendarTab, manages event loading, calendar state, and user interactions.
class _CalendarTabState extends State<CalendarTab> { class _CalendarTabState extends State<CalendarTab> {
CalendarFormat _calendarFormat = CalendarFormat.week; CalendarFormat _calendarFormat = CalendarFormat.week;
late DateTime _focusedDay; late DateTime _focusedDay;
@ -60,6 +66,7 @@ class _CalendarTabState extends State<CalendarTab> {
} }
} }
/// Initializes user data and loads events for the calendar.
Future<void> _initializeData() async { Future<void> _initializeData() async {
if (_currentUserId != null) { if (_currentUserId != null) {
final userDoc = await FirebaseFirestore.instance final userDoc = await FirebaseFirestore.instance
@ -76,6 +83,7 @@ class _CalendarTabState extends State<CalendarTab> {
} }
} }
/// Loads all training events for the current user (trainer or player).
Future<void> _loadEvents() async { Future<void> _loadEvents() async {
if (_userRole == null) return; if (_userRole == null) return;
@ -84,12 +92,14 @@ class _CalendarTabState extends State<CalendarTab> {
QuerySnapshot trainersSnapshot; QuerySnapshot trainersSnapshot;
if (_userRole == 'trainer') { if (_userRole == 'trainer') {
// Trainer: only their own trainings
trainersSnapshot = await FirebaseFirestore.instance trainersSnapshot = await FirebaseFirestore.instance
.collection('User') .collection('User')
.where('role', isEqualTo: 'trainer') .where('role', isEqualTo: 'trainer')
.where(FieldPath.documentId, isEqualTo: _currentUserId) .where(FieldPath.documentId, isEqualTo: _currentUserId)
.get(); .get();
} else { } else {
// Player: trainings from trainers in their club
final userDoc = await FirebaseFirestore.instance final userDoc = await FirebaseFirestore.instance
.collection('User') .collection('User')
.doc(_currentUserId) .doc(_currentUserId)
@ -127,7 +137,7 @@ class _CalendarTabState extends State<CalendarTab> {
final trainingTimes = trainerData['trainingTimes'] as Map<String, dynamic>? ?? {}; final trainingTimes = trainerData['trainingTimes'] as Map<String, dynamic>? ?? {};
final trainingDurations = trainerData['trainingDurations'] as Map<String, dynamic>? ?? {}; final trainingDurations = trainerData['trainingDurations'] as Map<String, dynamic>? ?? {};
// Lade alle Trainings für das Datum // Load all specific trainings for each date
trainings.forEach((dateString, trainingsList) { trainings.forEach((dateString, trainingsList) {
final date = DateTime.tryParse(dateString); final date = DateTime.tryParse(dateString);
if (date == null) return; if (date == null) return;
@ -144,6 +154,7 @@ class _CalendarTabState extends State<CalendarTab> {
return sum; return sum;
}); });
// Build the event map for this training
final event = { final event = {
'trainerName': trainerData['name'] ?? 'Unbekannter Trainer', 'trainerName': trainerData['name'] ?? 'Unbekannter Trainer',
'time': timeStr, 'time': timeStr,
@ -165,7 +176,7 @@ class _CalendarTabState extends State<CalendarTab> {
} }
}); });
// Füge regelmäßige Trainings hinzu // Add recurring weekly trainings for the year
final now = DateTime.now(); final now = DateTime.now();
final yearStart = DateTime(now.year, 1, 1); final yearStart = DateTime(now.year, 1, 1);
final yearEnd = DateTime(now.year + 1, 1, 1); final yearEnd = DateTime(now.year + 1, 1, 1);
@ -179,7 +190,7 @@ class _CalendarTabState extends State<CalendarTab> {
'Sonntag': 7, 'Sonntag': 7,
}; };
// Erstelle eine Set von gecancelten Daten für schnelleren Zugriff // Create a set of cancelled dates for fast lookup
final cancelledDates = cancelledTrainings final cancelledDates = cancelledTrainings
.where((cancelled) => cancelled is Map<String, dynamic>) .where((cancelled) => cancelled is Map<String, dynamic>)
.map((cancelled) => DateTime.parse((cancelled as Map<String, dynamic>)['date'] as String)) .map((cancelled) => DateTime.parse((cancelled as Map<String, dynamic>)['date'] as String))
@ -195,23 +206,23 @@ class _CalendarTabState extends State<CalendarTab> {
final duration = trainingDurations[day] ?? 60; final duration = trainingDurations[day] ?? 60;
final targetWeekday = weekdays[day] ?? 1; final targetWeekday = weekdays[day] ?? 1;
// Finde das erste gewünschte Wochentags-Datum ab Jahresanfang // Find the first desired weekday date from the start of the year
DateTime firstDate = yearStart; DateTime firstDate = yearStart;
while (firstDate.weekday != targetWeekday) { while (firstDate.weekday != targetWeekday) {
firstDate = firstDate.add(const Duration(days: 1)); firstDate = firstDate.add(const Duration(days: 1));
} }
// Generiere Trainings für das gesamte Jahr // Generate weekly trainings for the whole year
while (firstDate.isBefore(yearEnd)) { while (firstDate.isBefore(yearEnd)) {
final normalizedDate = DateTime(firstDate.year, firstDate.month, firstDate.day); final normalizedDate = DateTime(firstDate.year, firstDate.month, firstDate.day);
final dateString = normalizedDate.toIso8601String(); final dateString = normalizedDate.toIso8601String();
// Prüfe, ob das Training für dieses Datum gecancelt ist // Check if the training for this date is cancelled
final isCancelled = cancelledDates.any((cancelledDate) => final isCancelled = cancelledDates.any((cancelledDate) =>
isSameDay(cancelledDate, normalizedDate) isSameDay(cancelledDate, normalizedDate)
); );
// Prüfe, ob bereits ein spezifisches Training für dieses Datum existiert // Check if a specific training already exists for this date
final hasSpecificTraining = trainings.containsKey(dateString); final hasSpecificTraining = trainings.containsKey(dateString);
if (!isCancelled && !hasSpecificTraining) { if (!isCancelled && !hasSpecificTraining) {

View File

@ -1,8 +1,12 @@
// favorites_tab.dart
// This file contains the FavoritesTab widget, which displays the user's favorite exercises and allows filtering by category.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'training_detail_screen.dart'; import 'training_detail_screen.dart';
/// The FavoritesTab displays the user's favorite exercises and allows filtering by category.
class FavoritesTab extends StatefulWidget { class FavoritesTab extends StatefulWidget {
final String? categoryFilter; final String? categoryFilter;
const FavoritesTab({super.key, this.categoryFilter}); const FavoritesTab({super.key, this.categoryFilter});
@ -11,6 +15,7 @@ class FavoritesTab extends StatefulWidget {
State<FavoritesTab> createState() => _FavoritesTabState(); State<FavoritesTab> createState() => _FavoritesTabState();
} }
/// State for the FavoritesTab, manages category selection and favorite loading.
class _FavoritesTabState extends State<FavoritesTab> { class _FavoritesTabState extends State<FavoritesTab> {
static const List<String> _categories = [ static const List<String> _categories = [
'Aufwärmen & Mobilisation', 'Aufwärmen & Mobilisation',
@ -42,7 +47,7 @@ class _FavoritesTabState extends State<FavoritesTab> {
body: Column( body: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Filter-Chip-Leiste // Category filter chips
Padding( Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: Wrap( child: Wrap(
@ -84,7 +89,7 @@ class _FavoritesTabState extends State<FavoritesTab> {
return const Center(child: Text('Keine Favoriten gefunden')); return const Center(child: Text('Keine Favoriten gefunden'));
} }
// Lade alle Favoriten-Dokumente auf einmal // Load all favorite exercise documents at once
return FutureBuilder<List<DocumentSnapshot>>( return FutureBuilder<List<DocumentSnapshot>>(
future: Future.wait(allFavorites.map((id) => future: Future.wait(allFavorites.map((id) =>
FirebaseFirestore.instance.collection('Training').doc(id).get() FirebaseFirestore.instance.collection('Training').doc(id).get()
@ -97,7 +102,7 @@ class _FavoritesTabState extends State<FavoritesTab> {
return const Center(child: Text('Favoriten konnten nicht geladen werden')); return const Center(child: Text('Favoriten konnten nicht geladen werden'));
} }
// Filtere die Favoriten basierend auf der Kategorie // Filter favorites by selected category
final filteredFavorites = multiSnapshot.data!.where((doc) { final filteredFavorites = multiSnapshot.data!.where((doc) {
if (!doc.exists) return false; if (!doc.exists) return false;
if (_selectedCategory == null) return true; if (_selectedCategory == null) return true;

View File

@ -1,5 +1,6 @@
// screens/home_screen.dart // screens/home_screen.dart
// Enthält die BottomNavigationBar-Logik und Navigation zwischen den Hauptscreens // This file contains the main HomeScreen widget, which manages the bottom navigation bar and navigation between the main screens of the app.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
@ -10,6 +11,7 @@ import 'calendar_tab.dart';
import 'profile_tab.dart'; import 'profile_tab.dart';
import 'training_detail_screen.dart'; import 'training_detail_screen.dart';
/// The main home screen of the app, containing the bottom navigation bar and all main tabs.
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
final VoidCallback? onLogoutSuccess; final VoidCallback? onLogoutSuccess;
const HomeScreen({super.key, this.onLogoutSuccess}); const HomeScreen({super.key, this.onLogoutSuccess});
@ -18,6 +20,7 @@ class HomeScreen extends StatefulWidget {
State<HomeScreen> createState() => _HomeScreenState(); State<HomeScreen> createState() => _HomeScreenState();
} }
/// State for the HomeScreen, manages navigation and data loading for the home tab.
class _HomeScreenState extends State<HomeScreen> { class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0; int _selectedIndex = 0;
Map<String, dynamic>? _nextTraining; Map<String, dynamic>? _nextTraining;
@ -35,6 +38,7 @@ class _HomeScreenState extends State<HomeScreen> {
_loadSuggestions(); _loadSuggestions();
} }
/// Loads exercise suggestions for the user based on unused and highly rated exercises.
Future<void> _loadSuggestions() async { Future<void> _loadSuggestions() async {
setState(() { setState(() {
_isLoadingSuggestions = true; _isLoadingSuggestions = true;
@ -48,7 +52,7 @@ class _HomeScreenState extends State<HomeScreen> {
return; return;
} }
// 1. Hole alle vom User genutzten Übungs-IDs // 1. Get all exercise IDs used by the user
final userDoc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get(); final userDoc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get();
final Set<String> usedExerciseIds = {}; final Set<String> usedExerciseIds = {};
if (userDoc.exists) { if (userDoc.exists) {
@ -67,14 +71,14 @@ class _HomeScreenState extends State<HomeScreen> {
}); });
} }
// 2. Hole alle Übungen // 2. Get all exercises
final exercisesSnapshot = await FirebaseFirestore.instance.collection('Training').get(); final exercisesSnapshot = await FirebaseFirestore.instance.collection('Training').get();
final allExercises = exercisesSnapshot.docs.map((doc) { final allExercises = exercisesSnapshot.docs.map((doc) {
return {'id': doc.id, ...doc.data() as Map<String, dynamic>}; return {'id': doc.id, ...doc.data() as Map<String, dynamic>};
}).toList(); }).toList();
// 3. Filtere nach "nicht genutzt" und "gut bewertet" // 3. Filter for unused and highly rated exercises
final suggestions = allExercises.where((exercise) { final suggestions = allExercises.where((exercise) {
final isUsed = usedExerciseIds.contains(exercise['id']); final isUsed = usedExerciseIds.contains(exercise['id']);
final rating = (exercise['rating overall'] as num?)?.toDouble() ?? 0.0; final rating = (exercise['rating overall'] as num?)?.toDouble() ?? 0.0;
@ -91,11 +95,12 @@ class _HomeScreenState extends State<HomeScreen> {
} catch (e) { } catch (e) {
setState(() { setState(() {
_isLoadingSuggestions = false; _isLoadingSuggestions = false;
_suggestionsError = 'Fehler beim Laden.'; _suggestionsError = 'Error loading suggestions.';
}); });
} }
} }
/// Loads the next training event for the user, depending on their role and club.
Future<void> _loadNextTraining() async { Future<void> _loadNextTraining() async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
@ -115,14 +120,14 @@ class _HomeScreenState extends State<HomeScreen> {
QuerySnapshot trainersSnapshot; QuerySnapshot trainersSnapshot;
if (userRole == 'trainer') { if (userRole == 'trainer') {
// Trainer sieht nur seine eigenen Trainings // Trainer sees only their own trainings
trainersSnapshot = await FirebaseFirestore.instance trainersSnapshot = await FirebaseFirestore.instance
.collection('User') .collection('User')
.where('role', isEqualTo: 'trainer') .where('role', isEqualTo: 'trainer')
.where(FieldPath.documentId, isEqualTo: user.uid) .where(FieldPath.documentId, isEqualTo: user.uid)
.get(); .get();
} else { } else {
// Spieler sieht nur Trainings von Trainern seines Vereins // Player sees only trainings from trainers in their club
if (userClub == null || userClub.isEmpty) { if (userClub == null || userClub.isEmpty) {
setState(() { setState(() {
_nextTraining = null; _nextTraining = null;
@ -160,7 +165,7 @@ class _HomeScreenState extends State<HomeScreen> {
final daysUntilNext = _getDaysUntilNext(day, now.weekday); final daysUntilNext = _getDaysUntilNext(day, now.weekday);
final eventDate = DateTime(now.year, now.month, now.day + daysUntilNext, hour, minute); final eventDate = DateTime(now.year, now.month, now.day + daysUntilNext, hour, minute);
// Prüfe die nächsten 4 Wochen // Check the next 4 weeks
for (var i = 0; i < 4; i++) { for (var i = 0; i < 4; i++) {
final date = eventDate.add(Duration(days: i * 7)); final date = eventDate.add(Duration(days: i * 7));
final normalizedDate = DateTime(date.year, date.month, date.day); final normalizedDate = DateTime(date.year, date.month, date.day);
@ -180,7 +185,7 @@ class _HomeScreenState extends State<HomeScreen> {
'date': dateString, 'date': dateString,
'time': timeStr, 'time': timeStr,
'duration': trainingDurations[day] ?? 60, 'duration': trainingDurations[day] ?? 60,
'trainerName': trainerData['name'] ?? 'Unbekannter Trainer', 'trainerName': trainerData['name'] ?? 'Unknown Trainer',
'day': day, 'day': day,
}; };
} }
@ -200,6 +205,7 @@ class _HomeScreenState extends State<HomeScreen> {
} }
} }
/// Returns the number of days until the next occurrence of a given weekday.
int _getDaysUntilNext(String day, int currentWeekday) { int _getDaysUntilNext(String day, int currentWeekday) {
final weekdays = { final weekdays = {
'Montag': 1, 'Montag': 1,
@ -219,22 +225,25 @@ class _HomeScreenState extends State<HomeScreen> {
return daysUntilNext; return daysUntilNext;
} }
/// Checks if two DateTime objects represent the same day.
bool isSameDay(DateTime a, DateTime b) { bool isSameDay(DateTime a, DateTime b) {
return a.year == b.year && a.month == b.month && a.day == b.day; return a.year == b.year && a.month == b.month && a.day == b.day;
} }
/// Handles navigation bar item taps and reloads suggestions if needed.
void _onItemTapped(int index) { void _onItemTapped(int index) {
if (_selectedIndex != index) { if (_selectedIndex != index) {
setState(() { setState(() {
_selectedIndex = index; _selectedIndex = index;
}); });
// Wenn zum Home-Tab gewechselt wird, lade die Vorschläge neu // Reload suggestions when switching to the Home tab
if (index == 0) { if (index == 0) {
_loadSuggestions(); _loadSuggestions();
} }
} }
} }
/// List of main screens for the bottom navigation bar.
List<Widget> get _screens => [ List<Widget> get _screens => [
_buildHomeTab(), _buildHomeTab(),
const SearchTab(), const SearchTab(),
@ -271,6 +280,7 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
/// Builds the Home tab content, including next training, favorites, and suggestions.
Widget _buildHomeTab() { Widget _buildHomeTab() {
return SafeArea( return SafeArea(
child: SingleChildScrollView( child: SingleChildScrollView(
@ -440,6 +450,7 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
/// Builds a favorite category circle with icon and label.
Widget _buildFavoriteCircle(String label, IconData icon, Color color) { Widget _buildFavoriteCircle(String label, IconData icon, Color color) {
return Padding( return Padding(
padding: const EdgeInsets.only(right: 16.0), padding: const EdgeInsets.only(right: 16.0),
@ -474,6 +485,7 @@ class _HomeScreenState extends State<HomeScreen> {
); );
} }
/// Builds a suggestion card for an exercise.
Widget _buildSuggestionCard(Map<String, dynamic> exercise) { Widget _buildSuggestionCard(Map<String, dynamic> exercise) {
final category = exercise['category'] as String? ?? 'Sonstiges'; final category = exercise['category'] as String? ?? 'Sonstiges';
final title = exercise['title'] as String? ?? 'Unbekannte Übung'; final title = exercise['title'] as String? ?? 'Unbekannte Übung';
@ -553,6 +565,7 @@ class _HomeScreenState extends State<HomeScreen> {
} }
} }
/// Mapping of category names to icons.
const Map<String, IconData> categoryIcons = { const Map<String, IconData> categoryIcons = {
'Aufwärmen & Mobilisation': Icons.directions_run, 'Aufwärmen & Mobilisation': Icons.directions_run,
'Wurf- & Torabschluss': Icons.sports_handball, 'Wurf- & Torabschluss': Icons.sports_handball,
@ -562,6 +575,7 @@ const Map<String, IconData> categoryIcons = {
'Koordination': Icons.directions_walk, 'Koordination': Icons.directions_walk,
}; };
/// Mapping of category names to colors.
const Map<String, Color> categoryColors = { const Map<String, Color> categoryColors = {
'Aufwärmen & Mobilisation': Colors.deepOrange, 'Aufwärmen & Mobilisation': Colors.deepOrange,
'Wurf- & Torabschluss': Colors.orange, 'Wurf- & Torabschluss': Colors.orange,

View File

@ -1,7 +1,11 @@
// home_tab.dart
// This file contains the HomeTab widget, which displays a welcome message, a featured image, favorite categories, and exercise suggestions.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../widgets/category_circle.dart'; import '../widgets/category_circle.dart';
import '../widgets/exercise_card.dart'; import '../widgets/exercise_card.dart';
/// The HomeTab displays a welcome message, featured image, favorite categories, and exercise suggestions.
class HomeTab extends StatelessWidget { class HomeTab extends StatelessWidget {
const HomeTab({super.key}); const HomeTab({super.key});
@ -13,7 +17,7 @@ class HomeTab extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Begrüßung // Welcome message
const Center( const Center(
child: Text( child: Text(
'Hallo Trainer!', 'Hallo Trainer!',
@ -25,7 +29,7 @@ class HomeTab extends StatelessWidget {
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Bild mit Titel // Featured image with title overlay
Container( Container(
height: 200, height: 200,
width: double.infinity, width: double.infinity,
@ -52,7 +56,7 @@ class HomeTab extends StatelessWidget {
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Favoriten Kategorien // Favorite categories section
const Text( const Text(
'Favoriten', 'Favoriten',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
@ -77,7 +81,7 @@ class HomeTab extends StatelessWidget {
), ),
), ),
const SizedBox(height: 20), const SizedBox(height: 20),
// Vorschläge // Suggestions section
const Text( const Text(
'Vorschläge', 'Vorschläge',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
@ -88,6 +92,7 @@ class HomeTab extends StatelessWidget {
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
// PageView with exercise suggestion cards
PageView( PageView(
children: const [ children: const [
ExerciseCard( ExerciseCard(

View File

@ -1,7 +1,11 @@
// login_screen.dart
// This file contains the LoginScreen widget, which handles user authentication (login and registration) and role selection.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
/// The LoginScreen allows users to log in or register, and select their role (trainer or player).
class LoginScreen extends StatefulWidget { class LoginScreen extends StatefulWidget {
final void Function() onLoginSuccess; final void Function() onLoginSuccess;
const LoginScreen({super.key, required this.onLoginSuccess}); const LoginScreen({super.key, required this.onLoginSuccess});
@ -10,6 +14,7 @@ class LoginScreen extends StatefulWidget {
State<LoginScreen> createState() => _LoginScreenState(); State<LoginScreen> createState() => _LoginScreenState();
} }
/// State for the LoginScreen, manages form state, authentication, and error handling.
class _LoginScreenState extends State<LoginScreen> { class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
@ -20,6 +25,7 @@ class _LoginScreenState extends State<LoginScreen> {
bool _isLogin = true; bool _isLogin = true;
bool _isTrainer = false; bool _isTrainer = false;
/// Handles login or registration when the form is submitted.
Future<void> _submit() async { Future<void> _submit() async {
setState(() { _loading = true; _error = null; }); setState(() { _loading = true; _error = null; });
try { try {
@ -31,31 +37,29 @@ class _LoginScreenState extends State<LoginScreen> {
password: _passwordController.text.trim(), password: _passwordController.text.trim(),
); );
// Firestore-Check // Check if user profile exists in Firestore
final uid = cred.user!.uid; final uid = cred.user!.uid;
final userDoc = await FirebaseFirestore.instance.collection('User').doc(uid).get(); final userDoc = await FirebaseFirestore.instance.collection('User').doc(uid).get();
if (userDoc.exists) { if (userDoc.exists) {
widget.onLoginSuccess(); widget.onLoginSuccess();
} else { } else {
setState(() { _error = 'Kein Benutzerprofil in der Datenbank gefunden!'; }); setState(() { _error = 'No user profile found in the database!'; });
await FirebaseAuth.instance.signOut(); await FirebaseAuth.instance.signOut();
} }
} catch (e) { } catch (e) {
rethrow; rethrow;
} }
} else { } else {
// Registrierung // Registration
try { try {
UserCredential cred = await FirebaseAuth.instance.createUserWithEmailAndPassword( UserCredential cred = await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: _emailController.text.trim(), email: _emailController.text.trim(),
password: _passwordController.text.trim(), password: _passwordController.text.trim(),
); );
// User-Datensatz in Firestore anlegen // Create user document in Firestore
final uid = cred.user!.uid; final uid = cred.user!.uid;
await FirebaseFirestore.instance.collection('User').doc(uid).set({ await FirebaseFirestore.instance.collection('User').doc(uid).set({
'email': _emailController.text.trim(), 'email': _emailController.text.trim(),
'name': _nameController.text.trim(), 'name': _nameController.text.trim(),
@ -69,6 +73,7 @@ class _LoginScreenState extends State<LoginScreen> {
} }
} }
} on FirebaseAuthException catch (e) { } on FirebaseAuthException catch (e) {
// Handle Firebase authentication errors
String errorMessage; String errorMessage;
switch (e.code) { switch (e.code) {
case 'user-not-found': case 'user-not-found':
@ -81,7 +86,7 @@ class _LoginScreenState extends State<LoginScreen> {
errorMessage = 'Ungültige E-Mail-Adresse.'; errorMessage = 'Ungültige E-Mail-Adresse.';
break; break;
case 'user-disabled': case 'user-disabled':
errorMessage = 'Dieser Account wurde deaktiviert.'; errorMessage = 'Dieses Konto wurde deaktiviert.';
break; break;
case 'email-already-in-use': case 'email-already-in-use':
errorMessage = 'Diese E-Mail-Adresse wird bereits verwendet.'; errorMessage = 'Diese E-Mail-Adresse wird bereits verwendet.';
@ -90,14 +95,14 @@ class _LoginScreenState extends State<LoginScreen> {
errorMessage = 'Das Passwort ist zu schwach.'; errorMessage = 'Das Passwort ist zu schwach.';
break; break;
case 'operation-not-allowed': case 'operation-not-allowed':
errorMessage = 'Diese Operation ist nicht erlaubt.'; errorMessage = 'Diese Aktion ist nicht erlaubt.';
break; break;
default: default:
errorMessage = 'Ein Fehler ist aufgetreten: ${e.message}'; errorMessage = 'Ein Fehler ist aufgetreten: ${e.message}';
} }
setState(() { _error = errorMessage; }); setState(() { _error = errorMessage; });
} catch (e) { } catch (e) {
setState(() { _error = 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuchen Sie es später erneut.'; }); setState(() { _error = 'Ein unerwarteter Fehler ist aufgetreten. Bitte versuche es später erneut.'; });
} finally { } finally {
setState(() { _loading = false; }); setState(() { _loading = false; });
} }
@ -114,15 +119,18 @@ class _LoginScreenState extends State<LoginScreen> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text(_isLogin ? 'Login' : 'Registrieren', style: Theme.of(context).textTheme.headlineMedium), // Title
Text(_isLogin ? 'Anmeldung' : 'Registrieren', style: Theme.of(context).textTheme.headlineMedium),
const SizedBox(height: 32), const SizedBox(height: 32),
if (!_isLogin) ...[ if (!_isLogin) ...[
// Name field for registration
TextFormField( TextFormField(
controller: _nameController, controller: _nameController,
decoration: const InputDecoration(labelText: 'Name'), decoration: const InputDecoration(labelText: 'Name'),
validator: (v) => v != null && v.trim().isNotEmpty ? null : 'Name angeben', validator: (v) => v != null && v.trim().isNotEmpty ? null : 'Bitte gib deinen Namen ein',
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Role selection
Row( Row(
children: [ children: [
Checkbox( Checkbox(
@ -144,24 +152,28 @@ class _LoginScreenState extends State<LoginScreen> {
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
// Email field
TextFormField( TextFormField(
controller: _emailController, controller: _emailController,
decoration: const InputDecoration(labelText: 'E-Mail'), decoration: const InputDecoration(labelText: 'E-Mail'),
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
validator: (v) => v != null && v.contains('@') ? null : 'Gib eine gültige E-Mail ein', validator: (v) => v != null && v.contains('@') ? null : 'Bitte gib eine gültige E-Mail ein',
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Password field
TextFormField( TextFormField(
controller: _passwordController, controller: _passwordController,
decoration: const InputDecoration(labelText: 'Passwort'), decoration: const InputDecoration(labelText: 'Passwort'),
obscureText: true, obscureText: true,
validator: (v) => v != null && v.length >= 6 ? null : 'Mind. 6 Zeichen', validator: (v) => v != null && v.length >= 6 ? null : 'Mindestens 6 Zeichen',
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
if (_error != null) ...[ if (_error != null) ...[
// Error message
Text(_error!, style: const TextStyle(color: Colors.red)), Text(_error!, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
// Submit button
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
child: ElevatedButton( child: ElevatedButton(
@ -174,10 +186,11 @@ class _LoginScreenState extends State<LoginScreen> {
}, },
child: _loading child: _loading
? const CircularProgressIndicator() ? const CircularProgressIndicator()
: Text(_isLogin ? 'Login' : 'Registrieren'), : Text(_isLogin ? 'Anmelden' : 'Registrieren'),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Switch between login and registration
TextButton( TextButton(
onPressed: _loading onPressed: _loading
? null ? null
@ -187,7 +200,7 @@ class _LoginScreenState extends State<LoginScreen> {
_error = null; _error = null;
}); });
}, },
child: Text(_isLogin ? 'Noch keinen Account? Jetzt registrieren!' : 'Schon registriert? Jetzt einloggen!'), child: Text(_isLogin ? 'Noch kein Konto? Jetzt registrieren!' : 'Schon registriert? Jetzt anmelden!'),
), ),
], ],
), ),

View File

@ -3,7 +3,12 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
// profile_tab.dart
// This file contains the ProfileTab widget, which allows users to view and edit their profile information, including training times and durations. Trainers have additional options for managing training schedules.
/// The ProfileTab displays and allows editing of user profile information.
class ProfileTab extends StatefulWidget { class ProfileTab extends StatefulWidget {
/// Callback for when the user logs out successfully.
final VoidCallback? onLogoutSuccess; final VoidCallback? onLogoutSuccess;
const ProfileTab({super.key, this.onLogoutSuccess}); const ProfileTab({super.key, this.onLogoutSuccess});
@ -12,23 +17,35 @@ class ProfileTab extends StatefulWidget {
} }
class _ProfileTabState extends State<ProfileTab> { class _ProfileTabState extends State<ProfileTab> {
// Form key for validating the profile form.
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
// Indicates if the user is a trainer.
bool _isTrainer = false; bool _isTrainer = false;
// Indicates if data is currently loading.
bool _isLoading = false; bool _isLoading = false;
// Stores training times for each day.
Map<String, TimeOfDay?> _trainingTimes = {}; Map<String, TimeOfDay?> _trainingTimes = {};
// Stores training durations for each day.
Map<String, int> _trainingDurations = {}; Map<String, int> _trainingDurations = {};
// User's name.
String _name = ''; String _name = '';
// User's email.
String _email = ''; String _email = '';
// User's club.
String _club = ''; String _club = '';
// User's role (trainer or player).
String? _userRole; String? _userRole;
// Date the user joined.
DateTime? _joinDate; DateTime? _joinDate;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Load user data when the widget is initialized.
_loadUserData(); _loadUserData();
} }
/// Loads user data from Firebase and updates the state.
Future<void> _loadUserData() async { Future<void> _loadUserData() async {
final user = FirebaseAuth.instance.currentUser; final user = FirebaseAuth.instance.currentUser;
if (user == null) return; if (user == null) return;
@ -41,6 +58,7 @@ class _ProfileTabState extends State<ProfileTab> {
final trainingTimes = data['trainingTimes'] as Map<String, dynamic>? ?? {}; final trainingTimes = data['trainingTimes'] as Map<String, dynamic>? ?? {};
final trainingDurations = data['trainingDurations'] as Map<String, dynamic>? ?? {}; final trainingDurations = data['trainingDurations'] as Map<String, dynamic>? ?? {};
// Convert training times from string to TimeOfDay.
final convertedTrainingTimes = <String, TimeOfDay?>{}; final convertedTrainingTimes = <String, TimeOfDay?>{};
trainingTimes.forEach((key, value) { trainingTimes.forEach((key, value) {
if (value != null) { if (value != null) {
@ -79,6 +97,7 @@ class _ProfileTabState extends State<ProfileTab> {
} }
} }
/// Saves the training time and duration for a specific day to Firebase.
Future<void> _saveTrainingTime(String day, TimeOfDay? time, int? duration) async { Future<void> _saveTrainingTime(String day, TimeOfDay? time, int? duration) async {
if (time == null || duration == null) return; if (time == null || duration == null) return;
@ -89,13 +108,13 @@ class _ProfileTabState extends State<ProfileTab> {
final timeString = '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}'; final timeString = '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
// Update training times and durations // Update training times and durations in Firestore.
await FirebaseFirestore.instance.collection('User').doc(user.uid).update({ await FirebaseFirestore.instance.collection('User').doc(user.uid).update({
'trainingTimes.$day': timeString, 'trainingTimes.$day': timeString,
'trainingDurations.$day': duration, 'trainingDurations.$day': duration,
}); });
// Trainings ab 1. Januar des aktuellen Jahres bis 52 Wochen in die Zukunft anlegen // Create training entries from the start of the year for 52 weeks ahead.
final now = DateTime.now(); final now = DateTime.now();
final yearStart = DateTime(now.year, 1, 1); final yearStart = DateTime(now.year, 1, 1);
final weekdays = { final weekdays = {
@ -109,7 +128,7 @@ class _ProfileTabState extends State<ProfileTab> {
}; };
final targetWeekday = weekdays[day] ?? 1; final targetWeekday = weekdays[day] ?? 1;
// Finde das erste gewünschte Wochentags-Datum ab Jahresanfang // Find the first occurrence of the target weekday from the start of the year.
DateTime firstDate = yearStart; DateTime firstDate = yearStart;
while (firstDate.weekday != targetWeekday) { while (firstDate.weekday != targetWeekday) {
firstDate = firstDate.add(const Duration(days: 1)); firstDate = firstDate.add(const Duration(days: 1));
@ -123,20 +142,20 @@ class _ProfileTabState extends State<ProfileTab> {
newTrainingDates.add(dateString); newTrainingDates.add(dateString);
} }
// Load existing trainingExercises and cancelledTrainings // Load existing trainingExercises and cancelledTrainings from Firestore.
final userDoc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get(); final userDoc = await FirebaseFirestore.instance.collection('User').doc(user.uid).get();
final data = userDoc.data() ?? {}; final data = userDoc.data() ?? {};
final trainingExercises = Map<String, dynamic>.from(data['trainingExercises'] ?? {}); final trainingExercises = Map<String, dynamic>.from(data['trainingExercises'] ?? {});
final cancelledTrainings = List<Map<String, dynamic>>.from(data['cancelledTrainings'] ?? []); final cancelledTrainings = List<Map<String, dynamic>>.from(data['cancelledTrainings'] ?? []);
// Add empty training only if not already present // Add empty training only if not already present.
for (final dateString in newTrainingDates) { for (final dateString in newTrainingDates) {
if (!trainingExercises.containsKey(dateString)) { if (!trainingExercises.containsKey(dateString)) {
trainingExercises[dateString] = []; trainingExercises[dateString] = [];
} }
} }
// Remove cancelledTrainings for these dates // Remove cancelledTrainings for these dates.
cancelledTrainings.removeWhere((cancelled) => cancelledTrainings.removeWhere((cancelled) =>
cancelled is Map<String, dynamic> && cancelled is Map<String, dynamic> &&
cancelled.containsKey('date') && cancelled.containsKey('date') &&
@ -171,6 +190,7 @@ class _ProfileTabState extends State<ProfileTab> {
} }
} }
/// Removes the training time and duration for a specific day from Firebase.
Future<void> _removeTrainingTime(String day) async { Future<void> _removeTrainingTime(String day) async {
setState(() => _isLoading = true); setState(() => _isLoading = true);
try { try {
@ -205,6 +225,7 @@ class _ProfileTabState extends State<ProfileTab> {
} }
} }
/// Opens a time picker dialog for selecting a training time, then a dialog for duration.
Future<void> _selectTime(BuildContext context, String day) async { Future<void> _selectTime(BuildContext context, String day) async {
final TimeOfDay? picked = await showTimePicker( final TimeOfDay? picked = await showTimePicker(
context: context, context: context,
@ -223,6 +244,7 @@ class _ProfileTabState extends State<ProfileTab> {
} }
} }
/// Saves the user's profile data (name, club, training times, durations) to Firebase.
Future<void> _saveUserData() async { Future<void> _saveUserData() async {
if (FirebaseAuth.instance.currentUser == null) return; if (FirebaseAuth.instance.currentUser == null) return;
@ -268,6 +290,7 @@ class _ProfileTabState extends State<ProfileTab> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser; final user = FirebaseAuth.instance.currentUser;
if (user == null) { if (user == null) {
// Show message if user is not logged in.
return const Center(child: Text('Nicht eingeloggt')); return const Center(child: Text('Nicht eingeloggt'));
} }
@ -278,6 +301,7 @@ class _ProfileTabState extends State<ProfileTab> {
IconButton( IconButton(
icon: const Icon(Icons.logout), icon: const Icon(Icons.logout),
onPressed: () async { onPressed: () async {
// Log out the user and call the callback if provided.
await FirebaseAuth.instance.signOut(); await FirebaseAuth.instance.signOut();
if (widget.onLogoutSuccess != null) { if (widget.onLogoutSuccess != null) {
widget.onLogoutSuccess!(); widget.onLogoutSuccess!();
@ -297,6 +321,7 @@ class _ProfileTabState extends State<ProfileTab> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Card for personal information fields.
Card( Card(
elevation: 4, elevation: 4,
child: Padding( child: Padding(
@ -312,6 +337,7 @@ class _ProfileTabState extends State<ProfileTab> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Editable name field.
TextField( TextField(
controller: TextEditingController(text: _name), controller: TextEditingController(text: _name),
decoration: const InputDecoration( decoration: const InputDecoration(
@ -322,6 +348,7 @@ class _ProfileTabState extends State<ProfileTab> {
onChanged: (value) => _name = value, onChanged: (value) => _name = value,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Non-editable email field.
TextField( TextField(
controller: TextEditingController(text: _email), controller: TextEditingController(text: _email),
decoration: const InputDecoration( decoration: const InputDecoration(
@ -332,6 +359,7 @@ class _ProfileTabState extends State<ProfileTab> {
enabled: false, enabled: false,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Editable club field.
TextField( TextField(
controller: TextEditingController(text: _club), controller: TextEditingController(text: _club),
decoration: const InputDecoration( decoration: const InputDecoration(
@ -343,7 +371,7 @@ class _ProfileTabState extends State<ProfileTab> {
onChanged: (value) => _club = value, onChanged: (value) => _club = value,
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Rolle // Non-editable role field.
TextField( TextField(
controller: TextEditingController(text: _userRole == 'trainer' ? 'Trainer' : 'Spieler'), controller: TextEditingController(text: _userRole == 'trainer' ? 'Trainer' : 'Spieler'),
decoration: const InputDecoration( decoration: const InputDecoration(
@ -355,6 +383,7 @@ class _ProfileTabState extends State<ProfileTab> {
), ),
if (_joinDate != null) ...[ if (_joinDate != null) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
// Non-editable join date field.
TextField( TextField(
controller: TextEditingController(text: DateFormat('dd.MM.yyyy').format(_joinDate!)), controller: TextEditingController(text: DateFormat('dd.MM.yyyy').format(_joinDate!)),
decoration: const InputDecoration( decoration: const InputDecoration(
@ -371,6 +400,7 @@ class _ProfileTabState extends State<ProfileTab> {
), ),
if (_userRole == 'trainer') ...[ if (_userRole == 'trainer') ...[
const SizedBox(height: 24), const SizedBox(height: 24),
// Card for managing training times (visible only to trainers).
Card( Card(
elevation: 4, elevation: 4,
child: Padding( child: Padding(
@ -386,6 +416,7 @@ class _ProfileTabState extends State<ProfileTab> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// List of training days with time and duration controls.
...['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'] ...['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']
.map((day) => Card( .map((day) => Card(
margin: const EdgeInsets.only(bottom: 8), margin: const EdgeInsets.only(bottom: 8),
@ -400,6 +431,7 @@ class _ProfileTabState extends State<ProfileTab> {
trailing: Row( trailing: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Edit or add training time button.
IconButton( IconButton(
icon: Icon( icon: Icon(
_trainingTimes[day] != null _trainingTimes[day] != null
@ -411,6 +443,7 @@ class _ProfileTabState extends State<ProfileTab> {
: () => _selectTime(context, day), : () => _selectTime(context, day),
), ),
if (_trainingTimes[day] != null) if (_trainingTimes[day] != null)
// Remove training time button.
IconButton( IconButton(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
onPressed: _isLoading onPressed: _isLoading
@ -434,6 +467,7 @@ class _ProfileTabState extends State<ProfileTab> {
} }
} }
/// Dialog for selecting the duration of a training session.
class _DurationDialog extends StatefulWidget { class _DurationDialog extends StatefulWidget {
final int initialDuration; final int initialDuration;
@ -449,6 +483,7 @@ class _DurationDialogState extends State<_DurationDialog> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Initialize duration with the provided initial value.
_duration = widget.initialDuration; _duration = widget.initialDuration;
} }
@ -464,6 +499,7 @@ class _DurationDialogState extends State<_DurationDialog> {
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Decrement duration button (minimum 15 minutes).
IconButton( IconButton(
icon: const Icon(Icons.remove), icon: const Icon(Icons.remove),
onPressed: () { onPressed: () {
@ -476,6 +512,7 @@ class _DurationDialogState extends State<_DurationDialog> {
'$_duration Minuten', '$_duration Minuten',
style: const TextStyle(fontSize: 18), style: const TextStyle(fontSize: 18),
), ),
// Increment duration button (maximum 300 minutes).
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
onPressed: _duration < 300 onPressed: _duration < 300
@ -487,10 +524,12 @@ class _DurationDialogState extends State<_DurationDialog> {
], ],
), ),
actions: [ actions: [
// Cancel button.
TextButton( TextButton(
onPressed: () => Navigator.pop(context), onPressed: () => Navigator.pop(context),
child: const Text('Abbrechen'), child: const Text('Abbrechen'),
), ),
// Confirm button.
ElevatedButton( ElevatedButton(
onPressed: () => Navigator.pop(context, _duration), onPressed: () => Navigator.pop(context, _duration),
child: const Text('Bestätigen'), child: const Text('Bestätigen'),

View File

@ -6,8 +6,11 @@ import 'package:image_picker/image_picker.dart';
import 'dart:io'; import 'dart:io';
import 'training_detail_screen.dart'; import 'training_detail_screen.dart';
/// The SearchTab displays a searchable and filterable list of training exercises.
class SearchTab extends StatefulWidget { class SearchTab extends StatefulWidget {
/// If true, enables selection mode for choosing exercises.
final bool selectMode; final bool selectMode;
/// Remaining time for selection mode (optional).
final int? remainingTime; final int? remainingTime;
const SearchTab({ const SearchTab({
@ -21,7 +24,9 @@ class SearchTab extends StatefulWidget {
} }
class _SearchTabState extends State<SearchTab> { class _SearchTabState extends State<SearchTab> {
// Controller for the search input field.
final TextEditingController _searchController = TextEditingController(); final TextEditingController _searchController = TextEditingController();
// List of available exercise categories.
final List<String> _categories = [ final List<String> _categories = [
'Aufwärmen & Mobilisation', 'Aufwärmen & Mobilisation',
'Wurf- & Torabschluss', 'Wurf- & Torabschluss',
@ -30,15 +35,21 @@ class _SearchTabState extends State<SearchTab> {
'Pass', 'Pass',
'Koordination', 'Koordination',
]; ];
// Currently selected category for filtering.
String? _selectedCategory; String? _selectedCategory;
// Current search term entered by the user.
String _searchTerm = ''; String _searchTerm = '';
// Indicates if the user is a trainer.
bool _isTrainer = false; bool _isTrainer = false;
// Indicates if the trainer check has completed.
bool _trainerChecked = false; bool _trainerChecked = false;
// Set of favorite exercise IDs.
Set<String> _favorites = {}; Set<String> _favorites = {};
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Listen for changes in the search field and update the search term.
_searchController.addListener(() { _searchController.addListener(() {
setState(() { setState(() {
_searchTerm = _searchController.text.trim(); _searchTerm = _searchController.text.trim();
@ -48,6 +59,7 @@ class _SearchTabState extends State<SearchTab> {
_loadFavorites(); _loadFavorites();
} }
/// Checks if the current user is a trainer and updates state.
Future<void> _checkIfTrainer() async { Future<void> _checkIfTrainer() async {
final user = FirebaseAuth.instance.currentUser; final user = FirebaseAuth.instance.currentUser;
if (user == null) return; if (user == null) return;
@ -58,6 +70,7 @@ class _SearchTabState extends State<SearchTab> {
}); });
} }
/// Loads the user's favorite exercises from Firestore.
Future<void> _loadFavorites() async { Future<void> _loadFavorites() async {
final user = FirebaseAuth.instance.currentUser; final user = FirebaseAuth.instance.currentUser;
if (user == null) return; if (user == null) return;
@ -70,13 +83,15 @@ class _SearchTabState extends State<SearchTab> {
} }
} }
/// Shows a dialog for creating a new training exercise (trainer only).
void _showCreateTrainingDialog(BuildContext context) { void _showCreateTrainingDialog(BuildContext context) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => _CreateTrainingDialog(categories: _categories), builder: (context) => _CreateTrainingDialog(categories: _categories),
).then((_) => setState(() {})); // Refresh nach Hinzufügen ).then((_) => setState(() {})); // Refresh after adding
} }
/// Toggles the favorite status of an exercise for the current user.
Future<void> _toggleFavorite(String trainingId, bool isFavorite) async { Future<void> _toggleFavorite(String trainingId, bool isFavorite) async {
final user = FirebaseAuth.instance.currentUser; final user = FirebaseAuth.instance.currentUser;
if (user == null) return; if (user == null) return;
@ -89,7 +104,7 @@ class _SearchTabState extends State<SearchTab> {
'favorites': FieldValue.arrayUnion([trainingId]), 'favorites': FieldValue.arrayUnion([trainingId]),
}); });
} }
await _loadFavorites(); // Aktualisiere die Favoriten nach dem Toggle await _loadFavorites(); // Update favorites after toggling
} }
@override @override
@ -112,6 +127,7 @@ class _SearchTabState extends State<SearchTab> {
), ),
body: Column( body: Column(
children: [ children: [
// Search input field.
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: TextField( child: TextField(
@ -136,6 +152,7 @@ class _SearchTabState extends State<SearchTab> {
), ),
), ),
), ),
// Category filter chips.
Padding( Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
@ -164,6 +181,7 @@ class _SearchTabState extends State<SearchTab> {
], ],
), ),
), ),
// Exercise grid view.
Expanded( Expanded(
child: FutureBuilder<QuerySnapshot>( child: FutureBuilder<QuerySnapshot>(
future: FirebaseFirestore.instance.collection('Training').get(), future: FirebaseFirestore.instance.collection('Training').get(),
@ -180,6 +198,7 @@ class _SearchTabState extends State<SearchTab> {
return const Center(child: Text('Keine Übungen gefunden')); return const Center(child: Text('Keine Übungen gefunden'));
} }
// Filter exercises by search term and category.
var exercises = snapshot.data!.docs.where((doc) { var exercises = snapshot.data!.docs.where((doc) {
final data = doc.data() as Map<String, dynamic>; final data = doc.data() as Map<String, dynamic>;
final title = data['title']?.toString().toLowerCase() ?? ''; final title = data['title']?.toString().toLowerCase() ?? '';
@ -210,6 +229,7 @@ class _SearchTabState extends State<SearchTab> {
final doc = exercises[index]; final doc = exercises[index];
final data = doc.data() as Map<String, dynamic>; final data = doc.data() as Map<String, dynamic>;
final duration = (data['duration'] as num?)?.toInt() ?? 0; final duration = (data['duration'] as num?)?.toInt() ?? 0;
// Disable selection if duration exceeds remaining time in select mode.
final isDisabled = widget.selectMode && duration > (widget.remainingTime ?? 0); final isDisabled = widget.selectMode && duration > (widget.remainingTime ?? 0);
return Card( return Card(
@ -220,6 +240,7 @@ class _SearchTabState extends State<SearchTab> {
? null ? null
: () { : () {
if (widget.selectMode) { if (widget.selectMode) {
// Return selected exercise data in select mode.
Navigator.pop(context, { Navigator.pop(context, {
'id': doc.id, 'id': doc.id,
'title': data['title']?.toString() ?? 'Unbekannte Übung', 'title': data['title']?.toString() ?? 'Unbekannte Übung',
@ -227,6 +248,7 @@ class _SearchTabState extends State<SearchTab> {
'duration': duration, 'duration': duration,
}); });
} else { } else {
// Navigate to exercise detail screen.
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
@ -293,6 +315,7 @@ class _SearchTabState extends State<SearchTab> {
], ],
), ),
), ),
// Favorite icon button (toggle favorite status).
Positioned( Positioned(
top: 4, top: 4,
right: 4, right: 4,
@ -324,6 +347,7 @@ class _SearchTabState extends State<SearchTab> {
} }
} }
/// Dialog for creating a new training exercise.
class _CreateTrainingDialog extends StatefulWidget { class _CreateTrainingDialog extends StatefulWidget {
final List<String> categories; final List<String> categories;
const _CreateTrainingDialog({required this.categories}); const _CreateTrainingDialog({required this.categories});
@ -333,16 +357,26 @@ class _CreateTrainingDialog extends StatefulWidget {
} }
class _CreateTrainingDialogState extends State<_CreateTrainingDialog> { class _CreateTrainingDialogState extends State<_CreateTrainingDialog> {
// Form key for validating the create training form.
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
// Selected category for the new training.
String? _category; String? _category;
// Title of the new training.
String? _title; String? _title;
// Description of the new training.
String? _description; String? _description;
// Duration of the new training.
int? _duration; int? _duration;
// Difficulty level or year.
String? _year; String? _year;
// Indicates if the dialog is loading (creating training).
bool _loading = false; bool _loading = false;
// Selected image file for the training.
File? _imageFile; File? _imageFile;
// Image picker instance.
final _picker = ImagePicker(); final _picker = ImagePicker();
/// Opens the image picker to select an image from the gallery.
Future<void> _pickImage() async { Future<void> _pickImage() async {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery); final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
if (pickedFile != null) { if (pickedFile != null) {
@ -352,6 +386,7 @@ class _CreateTrainingDialogState extends State<_CreateTrainingDialog> {
} }
} }
/// Uploads the selected image to Firebase Storage and returns the download URL.
Future<String?> _uploadImage() async { Future<String?> _uploadImage() async {
if (_imageFile == null) return null; if (_imageFile == null) return null;

View File

@ -2,7 +2,9 @@ import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart'; import 'package:firebase_auth/firebase_auth.dart';
/// The TrainingDetailScreen displays details and ratings for a specific training exercise.
class TrainingDetailScreen extends StatefulWidget { class TrainingDetailScreen extends StatefulWidget {
/// The ID of the training exercise to display.
final String trainingId; final String trainingId;
const TrainingDetailScreen({super.key, required this.trainingId}); const TrainingDetailScreen({super.key, required this.trainingId});
@ -12,17 +14,23 @@ class TrainingDetailScreen extends StatefulWidget {
} }
class _TrainingDetailScreenState extends State<TrainingDetailScreen> { class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
// The current user's rating for this exercise.
double? _userRating; double? _userRating;
// Indicates if a loading operation is in progress.
bool _isLoading = false; bool _isLoading = false;
// Indicates if the current user is a player (not a trainer).
bool _isPlayer = false; bool _isPlayer = false;
// Indicates if the user role check has completed.
bool _userRoleChecked = false; bool _userRoleChecked = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// Check the user's role when the widget is initialized.
_checkUserRole(); _checkUserRole();
} }
/// Checks the current user's role (player or trainer) and updates state.
Future<void> _checkUserRole() async { Future<void> _checkUserRole() async {
final user = FirebaseAuth.instance.currentUser; final user = FirebaseAuth.instance.currentUser;
if (user == null) return; if (user == null) return;
@ -40,6 +48,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
} }
} }
/// Submits a rating for the current training exercise by the user.
Future<void> _submitRating(double rating) async { Future<void> _submitRating(double rating) async {
if (!_isPlayer) { if (!_isPlayer) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@ -65,23 +74,23 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
final data = trainingDoc.data() as Map<String, dynamic>; final data = trainingDoc.data() as Map<String, dynamic>;
List<dynamic> ratings = List<dynamic>.from(data['ratings'] ?? []); List<dynamic> ratings = List<dynamic>.from(data['ratings'] ?? []);
// Entferne alte Bewertung des Users falls vorhanden // Remove old rating from this user if present.
ratings.removeWhere((r) => r['userId'] == user.uid); ratings.removeWhere((r) => r['userId'] == user.uid);
// Füge neue Bewertung hinzu // Add new rating.
ratings.add({ ratings.add({
'userId': user.uid, 'userId': user.uid,
'rating': rating, 'rating': rating,
'timestamp': DateTime.now().toIso8601String(), // Verwende ISO-String statt FieldValue 'timestamp': DateTime.now().toIso8601String(), // Use ISO string for timestamp
}); });
// Berechne neue Gesamtbewertung // Calculate new overall rating.
double overallRating = 0; double overallRating = 0;
if (ratings.isNotEmpty) { if (ratings.isNotEmpty) {
overallRating = ratings.map((r) => (r['rating'] as num).toDouble()).reduce((a, b) => a + b) / ratings.length; overallRating = ratings.map((r) => (r['rating'] as num).toDouble()).reduce((a, b) => a + b) / ratings.length;
} }
// Aktualisiere das Dokument // Update the training document with new ratings and overall rating.
await trainingRef.update({ await trainingRef.update({
'ratings': ratings, 'ratings': ratings,
'rating overall': overallRating, 'rating overall': overallRating,
@ -123,7 +132,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
} }
final data = snapshot.data!.data() as Map<String, dynamic>; final data = snapshot.data!.data() as Map<String, dynamic>;
// Hole die Bewertung des aktuellen Users // Get the current user's rating for this exercise if not already loaded.
final user = FirebaseAuth.instance.currentUser; final user = FirebaseAuth.instance.currentUser;
if (user != null && _userRating == null) { if (user != null && _userRating == null) {
final ratings = List<dynamic>.from(data['ratings'] ?? []); final ratings = List<dynamic>.from(data['ratings'] ?? []);
@ -141,6 +150,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Display the exercise image if available.
Container( Container(
width: double.infinity, width: double.infinity,
height: 200, height: 200,
@ -155,6 +165,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Display the exercise title.
Text( Text(
data['title'] ?? 'Unbekannt', data['title'] ?? 'Unbekannt',
style: const TextStyle( style: const TextStyle(
@ -163,21 +174,25 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Display the exercise description.
Text( Text(
data['description'] ?? 'Keine Beschreibung', data['description'] ?? 'Keine Beschreibung',
style: TextStyle(color: Colors.grey[600]), style: TextStyle(color: Colors.grey[600]),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Display the exercise duration.
Text( Text(
'Dauer: ${data['duration'] ?? '-'} Minuten', 'Dauer: ${data['duration'] ?? '-'} Minuten',
style: TextStyle(color: Colors.grey[600]), style: TextStyle(color: Colors.grey[600]),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Display the exercise level/year.
Text( Text(
'Level: ${data['year'] ?? '-'}', 'Level: ${data['year'] ?? '-'}',
style: TextStyle(color: Colors.grey[600]), style: TextStyle(color: Colors.grey[600]),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
// Display the average rating.
Row( Row(
children: [ children: [
const Icon(Icons.star, color: Colors.amber), const Icon(Icons.star, color: Colors.amber),
@ -189,10 +204,12 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Display the number of ratings.
Text( Text(
'Anzahl Bewertungen: ${(data['ratings'] ?? []).length}', 'Anzahl Bewertungen: ${(data['ratings'] ?? []).length}',
style: TextStyle(color: Colors.grey[600]), style: TextStyle(color: Colors.grey[600]),
), ),
// Show rating controls if the user is a player.
if (_userRoleChecked && _isPlayer) ...[ if (_userRoleChecked && _isPlayer) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( const Text(
@ -222,6 +239,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
), ),
], ],
const SizedBox(height: 8), const SizedBox(height: 8),
// Display the exercise category.
Text( Text(
'Kategorie: ${data['category'] ?? '-'}', 'Kategorie: ${data['category'] ?? '-'}',
style: TextStyle(color: Colors.grey[600]), style: TextStyle(color: Colors.grey[600]),

View File

@ -1,28 +1,40 @@
// responsive.dart
// Utility class for handling responsive design in Flutter apps. Provides helpers for device type checks, scaling, and adaptive sizing.
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// The Responsive class provides static methods to help adapt UI to different screen sizes (mobile, tablet, desktop).
class Responsive { class Responsive {
/// Returns true if the device is considered a mobile (width < 600px).
static bool isMobile(BuildContext context) => static bool isMobile(BuildContext context) =>
MediaQuery.of(context).size.width < 600; MediaQuery.of(context).size.width < 600;
/// Returns true if the device is considered a tablet (600px <= width < 1200px).
static bool isTablet(BuildContext context) => static bool isTablet(BuildContext context) =>
MediaQuery.of(context).size.width >= 600 && MediaQuery.of(context).size.width >= 600 &&
MediaQuery.of(context).size.width < 1200; MediaQuery.of(context).size.width < 1200;
/// Returns true if the device is considered a desktop (width >= 1200px).
static bool isDesktop(BuildContext context) => static bool isDesktop(BuildContext context) =>
MediaQuery.of(context).size.width >= 1200; MediaQuery.of(context).size.width >= 1200;
/// Returns the current screen width in logical pixels.
static double getWidth(BuildContext context) => static double getWidth(BuildContext context) =>
MediaQuery.of(context).size.width; MediaQuery.of(context).size.width;
/// Returns the current screen height in logical pixels.
static double getHeight(BuildContext context) => static double getHeight(BuildContext context) =>
MediaQuery.of(context).size.height; MediaQuery.of(context).size.height;
/// Returns a width scaled by the given percentage of the screen width.
static double getScaledWidth(BuildContext context, double percentage) => static double getScaledWidth(BuildContext context, double percentage) =>
getWidth(context) * (percentage / 100); getWidth(context) * (percentage / 100);
/// Returns a height scaled by the given percentage of the screen height.
static double getScaledHeight(BuildContext context, double percentage) => static double getScaledHeight(BuildContext context, double percentage) =>
getHeight(context) * (percentage / 100); getHeight(context) * (percentage / 100);
/// Returns appropriate padding based on device type (mobile/tablet/desktop).
static EdgeInsets getPadding(BuildContext context) { static EdgeInsets getPadding(BuildContext context) {
if (isMobile(context)) { if (isMobile(context)) {
return const EdgeInsets.all(16.0); return const EdgeInsets.all(16.0);
@ -33,6 +45,7 @@ class Responsive {
} }
} }
/// Returns a font size scaled for the device type.
static double getFontSize(BuildContext context, double baseSize) { static double getFontSize(BuildContext context, double baseSize) {
if (isMobile(context)) { if (isMobile(context)) {
return baseSize; return baseSize;
@ -43,6 +56,7 @@ class Responsive {
} }
} }
/// Returns an icon size scaled for the device type.
static double getIconSize(BuildContext context, double baseSize) { static double getIconSize(BuildContext context, double baseSize) {
if (isMobile(context)) { if (isMobile(context)) {
return baseSize; return baseSize;

View File

@ -1,11 +1,17 @@
// widgets/category_circle.dart // widgets/category_circle.dart
// Widget for a circular category view
// Widget für eine runde Kategorie-Ansicht // Widget für eine runde Kategorie-Ansicht
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// A widget that displays a category as a circle with an icon and a title below.
/// Widget, das eine Kategorie als Kreis mit Icon und Titel darunter darstellt.
class CategoryCircle extends StatelessWidget { class CategoryCircle extends StatelessWidget {
/// The title of the category.
final String title; final String title;
/// The icon representing the category.
final IconData icon; final IconData icon;
/// Creates a CategoryCircle widget.
const CategoryCircle({super.key, required this.title, required this.icon}); const CategoryCircle({super.key, required this.title, required this.icon});
@override @override
@ -14,6 +20,7 @@ class CategoryCircle extends StatelessWidget {
padding: const EdgeInsets.only(right: 16.0), padding: const EdgeInsets.only(right: 16.0),
child: Column( child: Column(
children: [ children: [
// Circle with icon
// Kreis mit Icon // Kreis mit Icon
Container( Container(
width: 70, width: 70,
@ -29,6 +36,7 @@ class CategoryCircle extends StatelessWidget {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Title below the circle
// Titel unter dem Kreis // Titel unter dem Kreis
Text( Text(
title, title,

View File

@ -1,12 +1,19 @@
// widgets/exercise_card.dart // widgets/exercise_card.dart
// Widget for a training exercise card view
// Widget für eine Trainingskarten-Ansicht // Widget für eine Trainingskarten-Ansicht
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
/// A widget that displays a card for a training exercise, showing its icon, title, and category.
/// Widget, das eine Karte für eine Trainingsübung mit Icon, Titel und Kategorie anzeigt.
class ExerciseCard extends StatelessWidget { class ExerciseCard extends StatelessWidget {
/// The title of the exercise.
final String title; final String title;
/// The category of the exercise.
final String category; final String category;
/// The icon representing the exercise.
final IconData icon; final IconData icon;
/// Creates an ExerciseCard widget.
const ExerciseCard({ const ExerciseCard({
super.key, super.key,
required this.title, required this.title,
@ -26,9 +33,11 @@ class ExerciseCard extends StatelessWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// Icon for the exercise
// Icon für die Übung // Icon für die Übung
Icon(icon, size: 60, color: Theme.of(context).colorScheme.primary), Icon(icon, size: 60, color: Theme.of(context).colorScheme.primary),
const SizedBox(height: 16), const SizedBox(height: 16),
// Title of the exercise
// Titel der Übung // Titel der Übung
Text( Text(
title, title,
@ -36,6 +45,7 @@ class ExerciseCard extends StatelessWidget {
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// Category badge
// Kategorie-Badge // Kategorie-Badge
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),