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

View File

@ -1,13 +1,24 @@
// models/exercise.dart
// Data model for a training exercise
// Datenmodell für eine Trainingsübung
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 {
final String title; // Name der Übung
final String category; // Kategorie der Übung
final IconData icon; // Icon zur Darstellung
/// The name/title of the exercise.
// Name der Übung
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({
required this.title,
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:table_calendar/table_calendar.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 'package:uuid/uuid.dart';
/// Mapping of training categories to colors for calendar display.
const Map<String, Color> categoryColors = {
'Aufwärmen & Mobilisation': Colors.deepOrange,
'Wurf- & Torabschluss': Colors.orange,
@ -14,6 +18,7 @@ const Map<String, Color> categoryColors = {
'Koordination': Colors.teal,
};
/// The CalendarTab displays a calendar with all training events for the user.
class CalendarTab extends StatefulWidget {
final DateTime? initialDate;
const CalendarTab({super.key, this.initialDate});
@ -22,6 +27,7 @@ class CalendarTab extends StatefulWidget {
State<CalendarTab> createState() => _CalendarTabState();
}
/// State for the CalendarTab, manages event loading, calendar state, and user interactions.
class _CalendarTabState extends State<CalendarTab> {
CalendarFormat _calendarFormat = CalendarFormat.week;
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 {
if (_currentUserId != null) {
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 {
if (_userRole == null) return;
@ -84,12 +92,14 @@ class _CalendarTabState extends State<CalendarTab> {
QuerySnapshot trainersSnapshot;
if (_userRole == 'trainer') {
// Trainer: only their own trainings
trainersSnapshot = await FirebaseFirestore.instance
.collection('User')
.where('role', isEqualTo: 'trainer')
.where(FieldPath.documentId, isEqualTo: _currentUserId)
.get();
} else {
// Player: trainings from trainers in their club
final userDoc = await FirebaseFirestore.instance
.collection('User')
.doc(_currentUserId)
@ -127,7 +137,7 @@ class _CalendarTabState extends State<CalendarTab> {
final trainingTimes = trainerData['trainingTimes'] 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) {
final date = DateTime.tryParse(dateString);
if (date == null) return;
@ -144,6 +154,7 @@ class _CalendarTabState extends State<CalendarTab> {
return sum;
});
// Build the event map for this training
final event = {
'trainerName': trainerData['name'] ?? 'Unbekannter Trainer',
'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 yearStart = DateTime(now.year, 1, 1);
final yearEnd = DateTime(now.year + 1, 1, 1);
@ -179,7 +190,7 @@ class _CalendarTabState extends State<CalendarTab> {
'Sonntag': 7,
};
// Erstelle eine Set von gecancelten Daten für schnelleren Zugriff
// Create a set of cancelled dates for fast lookup
final cancelledDates = cancelledTrainings
.where((cancelled) => cancelled is Map<String, dynamic>)
.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 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;
while (firstDate.weekday != targetWeekday) {
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)) {
final normalizedDate = DateTime(firstDate.year, firstDate.month, firstDate.day);
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) =>
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);
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:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_auth/firebase_auth.dart';
import 'training_detail_screen.dart';
/// The FavoritesTab displays the user's favorite exercises and allows filtering by category.
class FavoritesTab extends StatefulWidget {
final String? categoryFilter;
const FavoritesTab({super.key, this.categoryFilter});
@ -11,6 +15,7 @@ class FavoritesTab extends StatefulWidget {
State<FavoritesTab> createState() => _FavoritesTabState();
}
/// State for the FavoritesTab, manages category selection and favorite loading.
class _FavoritesTabState extends State<FavoritesTab> {
static const List<String> _categories = [
'Aufwärmen & Mobilisation',
@ -42,7 +47,7 @@ class _FavoritesTabState extends State<FavoritesTab> {
body: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Filter-Chip-Leiste
// Category filter chips
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Wrap(
@ -84,7 +89,7 @@ class _FavoritesTabState extends State<FavoritesTab> {
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>>(
future: Future.wait(allFavorites.map((id) =>
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'));
}
// Filtere die Favoriten basierend auf der Kategorie
// Filter favorites by selected category
final filteredFavorites = multiSnapshot.data!.where((doc) {
if (!doc.exists) return false;
if (_selectedCategory == null) return true;

View File

@ -1,5 +1,6 @@
// 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:firebase_auth/firebase_auth.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
@ -10,6 +11,7 @@ import 'calendar_tab.dart';
import 'profile_tab.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 {
final VoidCallback? onLogoutSuccess;
const HomeScreen({super.key, this.onLogoutSuccess});
@ -18,6 +20,7 @@ class HomeScreen extends StatefulWidget {
State<HomeScreen> createState() => _HomeScreenState();
}
/// State for the HomeScreen, manages navigation and data loading for the home tab.
class _HomeScreenState extends State<HomeScreen> {
int _selectedIndex = 0;
Map<String, dynamic>? _nextTraining;
@ -35,6 +38,7 @@ class _HomeScreenState extends State<HomeScreen> {
_loadSuggestions();
}
/// Loads exercise suggestions for the user based on unused and highly rated exercises.
Future<void> _loadSuggestions() async {
setState(() {
_isLoadingSuggestions = true;
@ -48,7 +52,7 @@ class _HomeScreenState extends State<HomeScreen> {
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 Set<String> usedExerciseIds = {};
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 allExercises = exercisesSnapshot.docs.map((doc) {
return {'id': doc.id, ...doc.data() as Map<String, dynamic>};
}).toList();
// 3. Filtere nach "nicht genutzt" und "gut bewertet"
// 3. Filter for unused and highly rated exercises
final suggestions = allExercises.where((exercise) {
final isUsed = usedExerciseIds.contains(exercise['id']);
final rating = (exercise['rating overall'] as num?)?.toDouble() ?? 0.0;
@ -91,11 +95,12 @@ class _HomeScreenState extends State<HomeScreen> {
} catch (e) {
setState(() {
_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 {
setState(() => _isLoading = true);
try {
@ -115,14 +120,14 @@ class _HomeScreenState extends State<HomeScreen> {
QuerySnapshot trainersSnapshot;
if (userRole == 'trainer') {
// Trainer sieht nur seine eigenen Trainings
// Trainer sees only their own trainings
trainersSnapshot = await FirebaseFirestore.instance
.collection('User')
.where('role', isEqualTo: 'trainer')
.where(FieldPath.documentId, isEqualTo: user.uid)
.get();
} else {
// Spieler sieht nur Trainings von Trainern seines Vereins
// Player sees only trainings from trainers in their club
if (userClub == null || userClub.isEmpty) {
setState(() {
_nextTraining = null;
@ -160,7 +165,7 @@ class _HomeScreenState extends State<HomeScreen> {
final daysUntilNext = _getDaysUntilNext(day, now.weekday);
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++) {
final date = eventDate.add(Duration(days: i * 7));
final normalizedDate = DateTime(date.year, date.month, date.day);
@ -180,7 +185,7 @@ class _HomeScreenState extends State<HomeScreen> {
'date': dateString,
'time': timeStr,
'duration': trainingDurations[day] ?? 60,
'trainerName': trainerData['name'] ?? 'Unbekannter Trainer',
'trainerName': trainerData['name'] ?? 'Unknown Trainer',
'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) {
final weekdays = {
'Montag': 1,
@ -219,22 +225,25 @@ class _HomeScreenState extends State<HomeScreen> {
return daysUntilNext;
}
/// Checks if two DateTime objects represent the same day.
bool isSameDay(DateTime a, DateTime b) {
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) {
if (_selectedIndex != index) {
setState(() {
_selectedIndex = index;
});
// Wenn zum Home-Tab gewechselt wird, lade die Vorschläge neu
// Reload suggestions when switching to the Home tab
if (index == 0) {
_loadSuggestions();
}
}
}
/// List of main screens for the bottom navigation bar.
List<Widget> get _screens => [
_buildHomeTab(),
const SearchTab(),
@ -271,6 +280,7 @@ class _HomeScreenState extends State<HomeScreen> {
);
}
/// Builds the Home tab content, including next training, favorites, and suggestions.
Widget _buildHomeTab() {
return SafeArea(
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) {
return Padding(
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) {
final category = exercise['category'] as String? ?? 'Sonstiges';
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 = {
'Aufwärmen & Mobilisation': Icons.directions_run,
'Wurf- & Torabschluss': Icons.sports_handball,
@ -562,6 +575,7 @@ const Map<String, IconData> categoryIcons = {
'Koordination': Icons.directions_walk,
};
/// Mapping of category names to colors.
const Map<String, Color> categoryColors = {
'Aufwärmen & Mobilisation': Colors.deepOrange,
'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 '../widgets/category_circle.dart';
import '../widgets/exercise_card.dart';
/// The HomeTab displays a welcome message, featured image, favorite categories, and exercise suggestions.
class HomeTab extends StatelessWidget {
const HomeTab({super.key});
@ -13,7 +17,7 @@ class HomeTab extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Begrüßung
// Welcome message
const Center(
child: Text(
'Hallo Trainer!',
@ -25,7 +29,7 @@ class HomeTab extends StatelessWidget {
),
),
const SizedBox(height: 20),
// Bild mit Titel
// Featured image with title overlay
Container(
height: 200,
width: double.infinity,
@ -52,7 +56,7 @@ class HomeTab extends StatelessWidget {
),
),
const SizedBox(height: 20),
// Favoriten Kategorien
// Favorite categories section
const Text(
'Favoriten',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
@ -77,7 +81,7 @@ class HomeTab extends StatelessWidget {
),
),
const SizedBox(height: 20),
// Vorschläge
// Suggestions section
const Text(
'Vorschläge',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
@ -88,6 +92,7 @@ class HomeTab extends StatelessWidget {
child: Stack(
alignment: Alignment.center,
children: [
// PageView with exercise suggestion cards
PageView(
children: const [
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:firebase_auth/firebase_auth.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 {
final void Function() onLoginSuccess;
const LoginScreen({super.key, required this.onLoginSuccess});
@ -10,6 +14,7 @@ class LoginScreen extends StatefulWidget {
State<LoginScreen> createState() => _LoginScreenState();
}
/// State for the LoginScreen, manages form state, authentication, and error handling.
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
@ -20,6 +25,7 @@ class _LoginScreenState extends State<LoginScreen> {
bool _isLogin = true;
bool _isTrainer = false;
/// Handles login or registration when the form is submitted.
Future<void> _submit() async {
setState(() { _loading = true; _error = null; });
try {
@ -31,31 +37,29 @@ class _LoginScreenState extends State<LoginScreen> {
password: _passwordController.text.trim(),
);
// Firestore-Check
// Check if user profile exists in Firestore
final uid = cred.user!.uid;
final userDoc = await FirebaseFirestore.instance.collection('User').doc(uid).get();
if (userDoc.exists) {
widget.onLoginSuccess();
} else {
setState(() { _error = 'Kein Benutzerprofil in der Datenbank gefunden!'; });
setState(() { _error = 'No user profile found in the database!'; });
await FirebaseAuth.instance.signOut();
}
} catch (e) {
rethrow;
}
} else {
// Registrierung
// Registration
try {
UserCredential cred = await FirebaseAuth.instance.createUserWithEmailAndPassword(
email: _emailController.text.trim(),
password: _passwordController.text.trim(),
);
// User-Datensatz in Firestore anlegen
// Create user document in Firestore
final uid = cred.user!.uid;
await FirebaseFirestore.instance.collection('User').doc(uid).set({
'email': _emailController.text.trim(),
'name': _nameController.text.trim(),
@ -69,6 +73,7 @@ class _LoginScreenState extends State<LoginScreen> {
}
}
} on FirebaseAuthException catch (e) {
// Handle Firebase authentication errors
String errorMessage;
switch (e.code) {
case 'user-not-found':
@ -81,7 +86,7 @@ class _LoginScreenState extends State<LoginScreen> {
errorMessage = 'Ungültige E-Mail-Adresse.';
break;
case 'user-disabled':
errorMessage = 'Dieser Account wurde deaktiviert.';
errorMessage = 'Dieses Konto wurde deaktiviert.';
break;
case 'email-already-in-use':
errorMessage = 'Diese E-Mail-Adresse wird bereits verwendet.';
@ -90,14 +95,14 @@ class _LoginScreenState extends State<LoginScreen> {
errorMessage = 'Das Passwort ist zu schwach.';
break;
case 'operation-not-allowed':
errorMessage = 'Diese Operation ist nicht erlaubt.';
errorMessage = 'Diese Aktion ist nicht erlaubt.';
break;
default:
errorMessage = 'Ein Fehler ist aufgetreten: ${e.message}';
}
setState(() { _error = errorMessage; });
} 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 {
setState(() { _loading = false; });
}
@ -114,15 +119,18 @@ class _LoginScreenState extends State<LoginScreen> {
child: Column(
mainAxisSize: MainAxisSize.min,
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),
if (!_isLogin) ...[
// Name field for registration
TextFormField(
controller: _nameController,
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),
// Role selection
Row(
children: [
Checkbox(
@ -144,24 +152,28 @@ class _LoginScreenState extends State<LoginScreen> {
),
const SizedBox(height: 16),
],
// Email field
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'E-Mail'),
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),
// Password field
TextFormField(
controller: _passwordController,
decoration: const InputDecoration(labelText: 'Passwort'),
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),
if (_error != null) ...[
// Error message
Text(_error!, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 12),
],
// Submit button
SizedBox(
width: double.infinity,
child: ElevatedButton(
@ -174,10 +186,11 @@ class _LoginScreenState extends State<LoginScreen> {
},
child: _loading
? const CircularProgressIndicator()
: Text(_isLogin ? 'Login' : 'Registrieren'),
: Text(_isLogin ? 'Anmelden' : 'Registrieren'),
),
),
const SizedBox(height: 16),
// Switch between login and registration
TextButton(
onPressed: _loading
? null
@ -187,7 +200,7 @@ class _LoginScreenState extends State<LoginScreen> {
_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: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 {
/// Callback for when the user logs out successfully.
final VoidCallback? onLogoutSuccess;
const ProfileTab({super.key, this.onLogoutSuccess});
@ -12,23 +17,35 @@ class ProfileTab extends StatefulWidget {
}
class _ProfileTabState extends State<ProfileTab> {
// Form key for validating the profile form.
final _formKey = GlobalKey<FormState>();
// Indicates if the user is a trainer.
bool _isTrainer = false;
// Indicates if data is currently loading.
bool _isLoading = false;
// Stores training times for each day.
Map<String, TimeOfDay?> _trainingTimes = {};
// Stores training durations for each day.
Map<String, int> _trainingDurations = {};
// User's name.
String _name = '';
// User's email.
String _email = '';
// User's club.
String _club = '';
// User's role (trainer or player).
String? _userRole;
// Date the user joined.
DateTime? _joinDate;
@override
void initState() {
super.initState();
// Load user data when the widget is initialized.
_loadUserData();
}
/// Loads user data from Firebase and updates the state.
Future<void> _loadUserData() async {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
@ -41,6 +58,7 @@ class _ProfileTabState extends State<ProfileTab> {
final trainingTimes = data['trainingTimes'] as Map<String, dynamic>? ?? {};
final trainingDurations = data['trainingDurations'] as Map<String, dynamic>? ?? {};
// Convert training times from string to TimeOfDay.
final convertedTrainingTimes = <String, TimeOfDay?>{};
trainingTimes.forEach((key, value) {
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 {
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')}';
// Update training times and durations
// Update training times and durations in Firestore.
await FirebaseFirestore.instance.collection('User').doc(user.uid).update({
'trainingTimes.$day': timeString,
'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 yearStart = DateTime(now.year, 1, 1);
final weekdays = {
@ -109,7 +128,7 @@ class _ProfileTabState extends State<ProfileTab> {
};
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;
while (firstDate.weekday != targetWeekday) {
firstDate = firstDate.add(const Duration(days: 1));
@ -123,20 +142,20 @@ class _ProfileTabState extends State<ProfileTab> {
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 data = userDoc.data() ?? {};
final trainingExercises = Map<String, dynamic>.from(data['trainingExercises'] ?? {});
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) {
if (!trainingExercises.containsKey(dateString)) {
trainingExercises[dateString] = [];
}
}
// Remove cancelledTrainings for these dates
// Remove cancelledTrainings for these dates.
cancelledTrainings.removeWhere((cancelled) =>
cancelled is Map<String, dynamic> &&
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 {
setState(() => _isLoading = true);
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 {
final TimeOfDay? picked = await showTimePicker(
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 {
if (FirebaseAuth.instance.currentUser == null) return;
@ -268,6 +290,7 @@ class _ProfileTabState extends State<ProfileTab> {
Widget build(BuildContext context) {
final user = FirebaseAuth.instance.currentUser;
if (user == null) {
// Show message if user is not logged in.
return const Center(child: Text('Nicht eingeloggt'));
}
@ -278,6 +301,7 @@ class _ProfileTabState extends State<ProfileTab> {
IconButton(
icon: const Icon(Icons.logout),
onPressed: () async {
// Log out the user and call the callback if provided.
await FirebaseAuth.instance.signOut();
if (widget.onLogoutSuccess != null) {
widget.onLogoutSuccess!();
@ -297,6 +321,7 @@ class _ProfileTabState extends State<ProfileTab> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Card for personal information fields.
Card(
elevation: 4,
child: Padding(
@ -312,6 +337,7 @@ class _ProfileTabState extends State<ProfileTab> {
),
),
const SizedBox(height: 16),
// Editable name field.
TextField(
controller: TextEditingController(text: _name),
decoration: const InputDecoration(
@ -322,6 +348,7 @@ class _ProfileTabState extends State<ProfileTab> {
onChanged: (value) => _name = value,
),
const SizedBox(height: 16),
// Non-editable email field.
TextField(
controller: TextEditingController(text: _email),
decoration: const InputDecoration(
@ -332,6 +359,7 @@ class _ProfileTabState extends State<ProfileTab> {
enabled: false,
),
const SizedBox(height: 16),
// Editable club field.
TextField(
controller: TextEditingController(text: _club),
decoration: const InputDecoration(
@ -343,7 +371,7 @@ class _ProfileTabState extends State<ProfileTab> {
onChanged: (value) => _club = value,
),
const SizedBox(height: 16),
// Rolle
// Non-editable role field.
TextField(
controller: TextEditingController(text: _userRole == 'trainer' ? 'Trainer' : 'Spieler'),
decoration: const InputDecoration(
@ -355,6 +383,7 @@ class _ProfileTabState extends State<ProfileTab> {
),
if (_joinDate != null) ...[
const SizedBox(height: 16),
// Non-editable join date field.
TextField(
controller: TextEditingController(text: DateFormat('dd.MM.yyyy').format(_joinDate!)),
decoration: const InputDecoration(
@ -371,6 +400,7 @@ class _ProfileTabState extends State<ProfileTab> {
),
if (_userRole == 'trainer') ...[
const SizedBox(height: 24),
// Card for managing training times (visible only to trainers).
Card(
elevation: 4,
child: Padding(
@ -386,6 +416,7 @@ class _ProfileTabState extends State<ProfileTab> {
),
),
const SizedBox(height: 16),
// List of training days with time and duration controls.
...['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag']
.map((day) => Card(
margin: const EdgeInsets.only(bottom: 8),
@ -400,6 +431,7 @@ class _ProfileTabState extends State<ProfileTab> {
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Edit or add training time button.
IconButton(
icon: Icon(
_trainingTimes[day] != null
@ -411,6 +443,7 @@ class _ProfileTabState extends State<ProfileTab> {
: () => _selectTime(context, day),
),
if (_trainingTimes[day] != null)
// Remove training time button.
IconButton(
icon: const Icon(Icons.delete),
onPressed: _isLoading
@ -434,6 +467,7 @@ class _ProfileTabState extends State<ProfileTab> {
}
}
/// Dialog for selecting the duration of a training session.
class _DurationDialog extends StatefulWidget {
final int initialDuration;
@ -449,6 +483,7 @@ class _DurationDialogState extends State<_DurationDialog> {
@override
void initState() {
super.initState();
// Initialize duration with the provided initial value.
_duration = widget.initialDuration;
}
@ -464,6 +499,7 @@ class _DurationDialogState extends State<_DurationDialog> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Decrement duration button (minimum 15 minutes).
IconButton(
icon: const Icon(Icons.remove),
onPressed: () {
@ -476,6 +512,7 @@ class _DurationDialogState extends State<_DurationDialog> {
'$_duration Minuten',
style: const TextStyle(fontSize: 18),
),
// Increment duration button (maximum 300 minutes).
IconButton(
icon: const Icon(Icons.add),
onPressed: _duration < 300
@ -487,10 +524,12 @@ class _DurationDialogState extends State<_DurationDialog> {
],
),
actions: [
// Cancel button.
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Abbrechen'),
),
// Confirm button.
ElevatedButton(
onPressed: () => Navigator.pop(context, _duration),
child: const Text('Bestätigen'),

View File

@ -6,8 +6,11 @@ import 'package:image_picker/image_picker.dart';
import 'dart:io';
import 'training_detail_screen.dart';
/// The SearchTab displays a searchable and filterable list of training exercises.
class SearchTab extends StatefulWidget {
/// If true, enables selection mode for choosing exercises.
final bool selectMode;
/// Remaining time for selection mode (optional).
final int? remainingTime;
const SearchTab({
@ -21,7 +24,9 @@ class SearchTab extends StatefulWidget {
}
class _SearchTabState extends State<SearchTab> {
// Controller for the search input field.
final TextEditingController _searchController = TextEditingController();
// List of available exercise categories.
final List<String> _categories = [
'Aufwärmen & Mobilisation',
'Wurf- & Torabschluss',
@ -30,15 +35,21 @@ class _SearchTabState extends State<SearchTab> {
'Pass',
'Koordination',
];
// Currently selected category for filtering.
String? _selectedCategory;
// Current search term entered by the user.
String _searchTerm = '';
// Indicates if the user is a trainer.
bool _isTrainer = false;
// Indicates if the trainer check has completed.
bool _trainerChecked = false;
// Set of favorite exercise IDs.
Set<String> _favorites = {};
@override
void initState() {
super.initState();
// Listen for changes in the search field and update the search term.
_searchController.addListener(() {
setState(() {
_searchTerm = _searchController.text.trim();
@ -48,6 +59,7 @@ class _SearchTabState extends State<SearchTab> {
_loadFavorites();
}
/// Checks if the current user is a trainer and updates state.
Future<void> _checkIfTrainer() async {
final user = FirebaseAuth.instance.currentUser;
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 {
final user = FirebaseAuth.instance.currentUser;
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) {
showDialog(
context: context,
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 {
final user = FirebaseAuth.instance.currentUser;
if (user == null) return;
@ -89,7 +104,7 @@ class _SearchTabState extends State<SearchTab> {
'favorites': FieldValue.arrayUnion([trainingId]),
});
}
await _loadFavorites(); // Aktualisiere die Favoriten nach dem Toggle
await _loadFavorites(); // Update favorites after toggling
}
@override
@ -112,6 +127,7 @@ class _SearchTabState extends State<SearchTab> {
),
body: Column(
children: [
// Search input field.
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
@ -136,6 +152,7 @@ class _SearchTabState extends State<SearchTab> {
),
),
),
// Category filter chips.
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
@ -164,6 +181,7 @@ class _SearchTabState extends State<SearchTab> {
],
),
),
// Exercise grid view.
Expanded(
child: FutureBuilder<QuerySnapshot>(
future: FirebaseFirestore.instance.collection('Training').get(),
@ -180,6 +198,7 @@ class _SearchTabState extends State<SearchTab> {
return const Center(child: Text('Keine Übungen gefunden'));
}
// Filter exercises by search term and category.
var exercises = snapshot.data!.docs.where((doc) {
final data = doc.data() as Map<String, dynamic>;
final title = data['title']?.toString().toLowerCase() ?? '';
@ -210,6 +229,7 @@ class _SearchTabState extends State<SearchTab> {
final doc = exercises[index];
final data = doc.data() as Map<String, dynamic>;
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);
return Card(
@ -220,6 +240,7 @@ class _SearchTabState extends State<SearchTab> {
? null
: () {
if (widget.selectMode) {
// Return selected exercise data in select mode.
Navigator.pop(context, {
'id': doc.id,
'title': data['title']?.toString() ?? 'Unbekannte Übung',
@ -227,6 +248,7 @@ class _SearchTabState extends State<SearchTab> {
'duration': duration,
});
} else {
// Navigate to exercise detail screen.
Navigator.push(
context,
MaterialPageRoute(
@ -293,6 +315,7 @@ class _SearchTabState extends State<SearchTab> {
],
),
),
// Favorite icon button (toggle favorite status).
Positioned(
top: 4,
right: 4,
@ -324,6 +347,7 @@ class _SearchTabState extends State<SearchTab> {
}
}
/// Dialog for creating a new training exercise.
class _CreateTrainingDialog extends StatefulWidget {
final List<String> categories;
const _CreateTrainingDialog({required this.categories});
@ -333,16 +357,26 @@ class _CreateTrainingDialog extends StatefulWidget {
}
class _CreateTrainingDialogState extends State<_CreateTrainingDialog> {
// Form key for validating the create training form.
final _formKey = GlobalKey<FormState>();
// Selected category for the new training.
String? _category;
// Title of the new training.
String? _title;
// Description of the new training.
String? _description;
// Duration of the new training.
int? _duration;
// Difficulty level or year.
String? _year;
// Indicates if the dialog is loading (creating training).
bool _loading = false;
// Selected image file for the training.
File? _imageFile;
// Image picker instance.
final _picker = ImagePicker();
/// Opens the image picker to select an image from the gallery.
Future<void> _pickImage() async {
final pickedFile = await _picker.pickImage(source: ImageSource.gallery);
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 {
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:firebase_auth/firebase_auth.dart';
/// The TrainingDetailScreen displays details and ratings for a specific training exercise.
class TrainingDetailScreen extends StatefulWidget {
/// The ID of the training exercise to display.
final String trainingId;
const TrainingDetailScreen({super.key, required this.trainingId});
@ -12,17 +14,23 @@ class TrainingDetailScreen extends StatefulWidget {
}
class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
// The current user's rating for this exercise.
double? _userRating;
// Indicates if a loading operation is in progress.
bool _isLoading = false;
// Indicates if the current user is a player (not a trainer).
bool _isPlayer = false;
// Indicates if the user role check has completed.
bool _userRoleChecked = false;
@override
void initState() {
super.initState();
// Check the user's role when the widget is initialized.
_checkUserRole();
}
/// Checks the current user's role (player or trainer) and updates state.
Future<void> _checkUserRole() async {
final user = FirebaseAuth.instance.currentUser;
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 {
if (!_isPlayer) {
ScaffoldMessenger.of(context).showSnackBar(
@ -65,23 +74,23 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
final data = trainingDoc.data() as Map<String, dynamic>;
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);
// Füge neue Bewertung hinzu
// Add new rating.
ratings.add({
'userId': user.uid,
'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;
if (ratings.isNotEmpty) {
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({
'ratings': ratings,
'rating overall': overallRating,
@ -123,7 +132,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
}
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;
if (user != null && _userRating == null) {
final ratings = List<dynamic>.from(data['ratings'] ?? []);
@ -141,6 +150,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Display the exercise image if available.
Container(
width: double.infinity,
height: 200,
@ -155,6 +165,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
),
),
const SizedBox(height: 16),
// Display the exercise title.
Text(
data['title'] ?? 'Unbekannt',
style: const TextStyle(
@ -163,21 +174,25 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
),
),
const SizedBox(height: 8),
// Display the exercise description.
Text(
data['description'] ?? 'Keine Beschreibung',
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(height: 16),
// Display the exercise duration.
Text(
'Dauer: ${data['duration'] ?? '-'} Minuten',
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(height: 8),
// Display the exercise level/year.
Text(
'Level: ${data['year'] ?? '-'}',
style: TextStyle(color: Colors.grey[600]),
),
const SizedBox(height: 16),
// Display the average rating.
Row(
children: [
const Icon(Icons.star, color: Colors.amber),
@ -189,10 +204,12 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
],
),
const SizedBox(height: 8),
// Display the number of ratings.
Text(
'Anzahl Bewertungen: ${(data['ratings'] ?? []).length}',
style: TextStyle(color: Colors.grey[600]),
),
// Show rating controls if the user is a player.
if (_userRoleChecked && _isPlayer) ...[
const SizedBox(height: 16),
const Text(
@ -222,6 +239,7 @@ class _TrainingDetailScreenState extends State<TrainingDetailScreen> {
),
],
const SizedBox(height: 8),
// Display the exercise category.
Text(
'Kategorie: ${data['category'] ?? '-'}',
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';
/// The Responsive class provides static methods to help adapt UI to different screen sizes (mobile, tablet, desktop).
class Responsive {
/// Returns true if the device is considered a mobile (width < 600px).
static bool isMobile(BuildContext context) =>
MediaQuery.of(context).size.width < 600;
/// Returns true if the device is considered a tablet (600px <= width < 1200px).
static bool isTablet(BuildContext context) =>
MediaQuery.of(context).size.width >= 600 &&
MediaQuery.of(context).size.width < 1200;
/// Returns true if the device is considered a desktop (width >= 1200px).
static bool isDesktop(BuildContext context) =>
MediaQuery.of(context).size.width >= 1200;
/// Returns the current screen width in logical pixels.
static double getWidth(BuildContext context) =>
MediaQuery.of(context).size.width;
/// Returns the current screen height in logical pixels.
static double getHeight(BuildContext context) =>
MediaQuery.of(context).size.height;
/// Returns a width scaled by the given percentage of the screen width.
static double getScaledWidth(BuildContext context, double percentage) =>
getWidth(context) * (percentage / 100);
/// Returns a height scaled by the given percentage of the screen height.
static double getScaledHeight(BuildContext context, double percentage) =>
getHeight(context) * (percentage / 100);
/// Returns appropriate padding based on device type (mobile/tablet/desktop).
static EdgeInsets getPadding(BuildContext context) {
if (isMobile(context)) {
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) {
if (isMobile(context)) {
return baseSize;
@ -43,6 +56,7 @@ class Responsive {
}
}
/// Returns an icon size scaled for the device type.
static double getIconSize(BuildContext context, double baseSize) {
if (isMobile(context)) {
return baseSize;

View File

@ -1,11 +1,17 @@
// widgets/category_circle.dart
// Widget for a circular category view
// Widget für eine runde Kategorie-Ansicht
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 {
/// The title of the category.
final String title;
/// The icon representing the category.
final IconData icon;
/// Creates a CategoryCircle widget.
const CategoryCircle({super.key, required this.title, required this.icon});
@override
@ -14,6 +20,7 @@ class CategoryCircle extends StatelessWidget {
padding: const EdgeInsets.only(right: 16.0),
child: Column(
children: [
// Circle with icon
// Kreis mit Icon
Container(
width: 70,
@ -29,6 +36,7 @@ class CategoryCircle extends StatelessWidget {
),
),
const SizedBox(height: 8),
// Title below the circle
// Titel unter dem Kreis
Text(
title,

View File

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