Telas de login e dashboard de estudante feito
This commit is contained in:
60
lib/core/constants/app_constants.dart
Normal file
60
lib/core/constants/app_constants.dart
Normal file
@@ -0,0 +1,60 @@
|
||||
class AppConstants {
|
||||
// App Info
|
||||
static const String appName = 'AI Study Assistant';
|
||||
static const String appVersion = '1.0.0';
|
||||
|
||||
// Firebase Configuration
|
||||
static const String firebaseProjectId = 'teachit-dev-12345';
|
||||
|
||||
// API Configuration
|
||||
static const String apiBaseUrl = 'http://localhost:5001';
|
||||
static const String apiVersion = 'v1';
|
||||
|
||||
// Storage Keys
|
||||
static const String userTokenKey = 'user_token';
|
||||
static const String userPreferencesKey = 'user_preferences';
|
||||
static const String themeKey = 'theme_mode';
|
||||
|
||||
// Animation Durations
|
||||
static const Duration shortAnimation = Duration(milliseconds: 200);
|
||||
static const Duration mediumAnimation = Duration(milliseconds: 300);
|
||||
static const Duration longAnimation = Duration(milliseconds: 500);
|
||||
|
||||
// Spacing
|
||||
static const double spacingXS = 4.0;
|
||||
static const double spacingS = 8.0;
|
||||
static const double spacingM = 16.0;
|
||||
static const double spacingL = 24.0;
|
||||
static const double spacingXL = 32.0;
|
||||
|
||||
// Border Radius
|
||||
static const double radiusXS = 4.0;
|
||||
static const double radiusS = 8.0;
|
||||
static const double radiusM = 12.0;
|
||||
static const double radiusL = 16.0;
|
||||
static const double radiusXL = 20.0;
|
||||
|
||||
// Screen Breakpoints
|
||||
static const double mobileBreakpoint = 600.0;
|
||||
static const double tabletBreakpoint = 1024.0;
|
||||
static const double desktopBreakpoint = 1440.0;
|
||||
|
||||
// Pagination
|
||||
static const int defaultPageSize = 20;
|
||||
static const int maxPageSize = 100;
|
||||
|
||||
// File Upload Limits
|
||||
static const int maxFileSize = 50 * 1024 * 1024; // 50MB
|
||||
static const List<String> allowedFileTypes = [
|
||||
'pdf', 'doc', 'docx', 'txt', 'jpg', 'jpeg', 'png', 'gif', 'mp4', 'avi', 'mov'
|
||||
];
|
||||
|
||||
// Chat Configuration
|
||||
static const int maxMessageLength = 500;
|
||||
static const int maxChatHistory = 50;
|
||||
|
||||
// Quiz Configuration
|
||||
static const int defaultQuizTimeLimit = 30; // minutes
|
||||
static const int maxQuestionsPerQuiz = 50;
|
||||
static const double passingScore = 0.7; // 70%
|
||||
}
|
||||
177
lib/core/routing/app_router.dart
Normal file
177
lib/core/routing/app_router.dart
Normal file
@@ -0,0 +1,177 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../features/auth/presentation/pages/login_page.dart';
|
||||
import '../../features/auth/presentation/pages/signup_page.dart';
|
||||
import '../../features/dashboard/presentation/pages/student_dashboard_page.dart';
|
||||
import '../../features/dashboard/presentation/pages/teacher_dashboard_page.dart';
|
||||
import '../../features/tutor/presentation/pages/tutor_chat_page.dart';
|
||||
import '../../features/quiz/presentation/pages/quiz_list_page.dart';
|
||||
import '../../features/quiz/presentation/pages/quiz_page.dart';
|
||||
import '../../features/profile/presentation/pages/profile_page.dart';
|
||||
import '../../features/splash/presentation/pages/splash_page.dart';
|
||||
import '../../features/auth/presentation/pages/role_selection_page.dart';
|
||||
import '../../shared/presentation/pages/not_found_page.dart';
|
||||
|
||||
/// App Router Configuration
|
||||
class AppRouter {
|
||||
static const String splash = '/splash';
|
||||
static const String roleSelection = '/role-selection';
|
||||
static const String login = '/login';
|
||||
static const String signup = '/signup';
|
||||
static const String studentDashboard = '/student-dashboard';
|
||||
static const String teacherDashboard = '/teacher-dashboard';
|
||||
static const String tutor = '/tutor';
|
||||
static const String quizList = '/quiz';
|
||||
static const String quiz = '/quiz/:quizId';
|
||||
static const String profile = '/profile';
|
||||
|
||||
// Nested route paths (without leading slash)
|
||||
static const String tutorNested = 'tutor';
|
||||
static const String quizListNested = 'quiz';
|
||||
static const String quizNested = 'quiz/:quizId';
|
||||
|
||||
static final GoRouter router = GoRouter(
|
||||
initialLocation: splash,
|
||||
debugLogDiagnostics: true,
|
||||
errorBuilder: (context, state) => const NotFoundPage(),
|
||||
|
||||
routes: [
|
||||
// Splash Screen
|
||||
GoRoute(
|
||||
path: splash,
|
||||
name: 'splash',
|
||||
builder: (context, state) => const SplashPage(),
|
||||
),
|
||||
|
||||
// Role Selection
|
||||
GoRoute(
|
||||
path: roleSelection,
|
||||
name: 'roleSelection',
|
||||
builder: (context, state) => const RoleSelectionPage(),
|
||||
),
|
||||
|
||||
// Authentication Routes
|
||||
GoRoute(
|
||||
path: login,
|
||||
name: 'login',
|
||||
builder: (context, state) => const LoginPage(),
|
||||
),
|
||||
|
||||
GoRoute(
|
||||
path: signup,
|
||||
name: 'signup',
|
||||
builder: (context, state) => const SignupPage(),
|
||||
),
|
||||
|
||||
// Dashboard Routes
|
||||
GoRoute(
|
||||
path: studentDashboard,
|
||||
name: 'studentDashboard',
|
||||
builder: (context, state) => const StudentDashboardPage(),
|
||||
routes: [
|
||||
// Nested routes for student features
|
||||
GoRoute(
|
||||
path: tutorNested,
|
||||
name: 'studentTutor',
|
||||
builder: (context, state) => const TutorChatPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: quizListNested,
|
||||
name: 'quizList',
|
||||
builder: (context, state) => const QuizListPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: quizNested,
|
||||
name: 'quiz',
|
||||
builder: (context, state) {
|
||||
final quizId = state.pathParameters['quizId']!;
|
||||
return QuizPage(quizId: quizId);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
GoRoute(
|
||||
path: teacherDashboard,
|
||||
name: 'teacherDashboard',
|
||||
builder: (context, state) => const TeacherDashboardPage(),
|
||||
routes: [
|
||||
// Nested routes for teacher features
|
||||
GoRoute(
|
||||
path: tutorNested,
|
||||
name: 'teacherTutor',
|
||||
builder: (context, state) => const TutorChatPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: quizListNested,
|
||||
name: 'teacherQuizList',
|
||||
builder: (context, state) => const QuizListPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: quizNested,
|
||||
name: 'teacherQuiz',
|
||||
builder: (context, state) {
|
||||
final quizId = state.pathParameters['quizId']!;
|
||||
return QuizPage(quizId: quizId);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Profile Route
|
||||
GoRoute(
|
||||
path: profile,
|
||||
name: 'profile',
|
||||
builder: (context, state) => const ProfilePage(),
|
||||
),
|
||||
],
|
||||
|
||||
// Redirect unauthenticated users to login
|
||||
redirect: (context, state) {
|
||||
// TODO: Implement authentication check
|
||||
// For now, allow all routes
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
// Navigation helpers
|
||||
static void goToLogin(BuildContext context) {
|
||||
context.go(login);
|
||||
}
|
||||
|
||||
static void goToSignup(BuildContext context) {
|
||||
context.go(signup);
|
||||
}
|
||||
|
||||
static void goToStudentDashboard(BuildContext context) {
|
||||
context.go(studentDashboard);
|
||||
}
|
||||
|
||||
static void goToTeacherDashboard(BuildContext context) {
|
||||
context.go(teacherDashboard);
|
||||
}
|
||||
|
||||
static void goToTutor(BuildContext context) {
|
||||
context.go(tutor);
|
||||
}
|
||||
|
||||
static void goToQuizList(BuildContext context) {
|
||||
context.go(quizList);
|
||||
}
|
||||
|
||||
static void goToQuiz(BuildContext context, String quizId) {
|
||||
context.go('$quiz/$quizId');
|
||||
}
|
||||
|
||||
static void goToProfile(BuildContext context) {
|
||||
context.go(profile);
|
||||
}
|
||||
|
||||
static void goBack(BuildContext context) {
|
||||
context.pop();
|
||||
}
|
||||
|
||||
static void replaceWith(BuildContext context, String location) {
|
||||
context.go(location);
|
||||
}
|
||||
}
|
||||
128
lib/core/services/auth_service.dart
Normal file
128
lib/core/services/auth_service.dart
Normal file
@@ -0,0 +1,128 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
|
||||
/// Service for handling Firebase Authentication
|
||||
class AuthService {
|
||||
static final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
|
||||
/// Get current user
|
||||
static User? get currentUser {
|
||||
return _auth.currentUser;
|
||||
}
|
||||
|
||||
/// Get auth state changes stream
|
||||
static Stream<User?> get authStateChanges {
|
||||
return _auth.authStateChanges();
|
||||
}
|
||||
|
||||
/// Sign up with email and password
|
||||
static Future<UserCredential?> signUpWithEmailAndPassword({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
try {
|
||||
print('DEBUG: Tentando criar conta para email: $email');
|
||||
print('DEBUG: Password length: ${password.length}');
|
||||
|
||||
UserCredential result = await _auth.createUserWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
print('DEBUG: Conta criada com sucesso para: ${result.user?.email}');
|
||||
print('DEBUG: User ID: ${result.user?.uid}');
|
||||
print('DEBUG: Email verified: ${result.user?.emailVerified}');
|
||||
|
||||
// Verificar se o email foi verificado
|
||||
if (result.user != null && !result.user!.emailVerified) {
|
||||
print('DEBUG: Email não verificado, tentando enviar verificação...');
|
||||
await result.user!.sendEmailVerification();
|
||||
print('DEBUG: Email de verificação enviado');
|
||||
}
|
||||
|
||||
return result;
|
||||
} on FirebaseAuthException catch (e) {
|
||||
print('DEBUG: Erro Firebase ao criar conta: ${e.code} - ${e.message}');
|
||||
String errorMessage = _getErrorMessage(e.code);
|
||||
print('DEBUG: Mensagem de erro: $errorMessage');
|
||||
throw Exception(errorMessage);
|
||||
} catch (e) {
|
||||
print('DEBUG: Erro genérico ao criar conta: $e');
|
||||
throw Exception('Ocorreu um problema. Tente novamente');
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign in with email and password
|
||||
static Future<UserCredential?> signInWithEmailAndPassword({
|
||||
required String email,
|
||||
required String password,
|
||||
}) async {
|
||||
try {
|
||||
print('DEBUG: Tentando login para email: $email');
|
||||
print('DEBUG: Password length: ${password.length}');
|
||||
|
||||
// Verificar se há usuário atual
|
||||
User? currentUser = _auth.currentUser;
|
||||
print('DEBUG: Usuário atual: ${currentUser?.email}');
|
||||
|
||||
UserCredential result = await _auth.signInWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
print('DEBUG: Login realizado com sucesso para: ${result.user?.email}');
|
||||
print('DEBUG: User ID: ${result.user?.uid}');
|
||||
print('DEBUG: Email verified: ${result.user?.emailVerified}');
|
||||
|
||||
return result;
|
||||
} on FirebaseAuthException catch (e) {
|
||||
print('DEBUG: Erro Firebase ao fazer login: ${e.code} - ${e.message}');
|
||||
String errorMessage = _getErrorMessage(e.code);
|
||||
print('DEBUG: Mensagem de erro: $errorMessage');
|
||||
throw Exception(errorMessage);
|
||||
} catch (e) {
|
||||
print('DEBUG: Erro genérico ao fazer login: $e');
|
||||
throw Exception('Ocorreu um problema. Tente novamente');
|
||||
}
|
||||
}
|
||||
|
||||
/// Sign out
|
||||
static Future<void> signOut() async {
|
||||
try {
|
||||
print('DEBUG: Tentando fazer logout');
|
||||
await _auth.signOut();
|
||||
print('DEBUG: Logout realizado com sucesso');
|
||||
} catch (e) {
|
||||
print('DEBUG: Erro ao fazer logout: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get user-friendly error message
|
||||
static String _getErrorMessage(String code) {
|
||||
print('DEBUG: Processando código de erro: $code');
|
||||
|
||||
switch (code) {
|
||||
case 'weak-password':
|
||||
return 'A palavra-passe é muito fraca. Use pelo menos 8 caracteres.';
|
||||
case 'invalid-email':
|
||||
return 'O email fornecido é inválido. Verifique o formato.';
|
||||
case 'user-disabled':
|
||||
return 'Esta conta foi desativada. Contacte o suporte.';
|
||||
case 'user-not-found':
|
||||
return 'Nenhum utilizador encontrado com este email. Verifique os dados.';
|
||||
case 'wrong-password':
|
||||
return 'Palavra-passe incorreta. Tente novamente.';
|
||||
case 'email-already-in-use':
|
||||
return 'O email inserido já se encontra registrado';
|
||||
case 'operation-not-allowed':
|
||||
return 'Operação não permitida. Tente novamente.';
|
||||
case 'invalid-credential':
|
||||
return 'Credenciais inválidos. Verifique email e palavra-passe.';
|
||||
case 'too-many-requests':
|
||||
return 'Muitas tentativas. Aguarde alguns minutos antes de tentar novamente.';
|
||||
case 'network-request-failed':
|
||||
return 'Falha de conexão. Verifique sua internet e tente novamente.';
|
||||
default:
|
||||
return 'Ocorreu um problema. Tente novamente';
|
||||
}
|
||||
}
|
||||
}
|
||||
17
lib/core/services/firebase/firebase_service.dart
Normal file
17
lib/core/services/firebase/firebase_service.dart
Normal file
@@ -0,0 +1,17 @@
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
|
||||
/// Firebase Service - Central configuration and initialization
|
||||
class FirebaseService {
|
||||
/// Initialize Firebase services
|
||||
static Future<void> initialize() async {
|
||||
try {
|
||||
// Initialize Firebase Core (uses google-services.json automatically)
|
||||
await Firebase.initializeApp();
|
||||
|
||||
print('✅ Firebase initialized successfully');
|
||||
} catch (e) {
|
||||
print('❌ Firebase initialization failed: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
68
lib/core/theme/app_colors.dart
Normal file
68
lib/core/theme/app_colors.dart
Normal file
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// EPVC School Color Palette - New Color Scheme
|
||||
class AppColors {
|
||||
// Primary Brand Colors
|
||||
static const Color primaryTeal = Color(
|
||||
0xFF82C9BD,
|
||||
); // Main teal color - PRIMARY
|
||||
static const Color primaryOrange = Color(
|
||||
0xFFF68D2D,
|
||||
); // Accent orange - SECONDARY
|
||||
|
||||
// Gradient Colors
|
||||
static const Color gradientStart = Color(0xFF82C9BD); // Teal gradient start
|
||||
static const Color gradientEnd = Color(
|
||||
0xFF6AB8A8,
|
||||
); // Darker teal gradient end
|
||||
|
||||
// Secondary Colors
|
||||
static const Color secondaryTeal = Color(0xFF6AB8A8); // Darker teal
|
||||
static const Color accentTeal = Color(0xFF5AA69A); // Lighter teal accent
|
||||
static const Color lightOrange = Color(0xFFF7A960); // Lighter orange
|
||||
|
||||
// Neutral Colors
|
||||
static const Color background = Color(0xFFF8F9FA); // Light gray background
|
||||
static const Color surface = Color(0xFFFFFFFF); // White surfaces
|
||||
static const Color cardBackground = Color(0xFFFFFFFF); // White cards
|
||||
|
||||
// Text Colors
|
||||
static const Color textPrimary = Color(0xFF1A1A1A); // Primary text
|
||||
static const Color textSecondary = Color(0xFF6B7280); // Secondary text
|
||||
static const Color textHint = Color(0xFF9CA3AF); // Hint text
|
||||
|
||||
// Status Colors
|
||||
static const Color success = Color(0xFF10B981); // Green for success
|
||||
static const Color warning = Color(0xFFF59E0B); // Amber for warnings
|
||||
static const Color error = Color(0xFFEF4444); // Red for errors
|
||||
static const Color info = Color(0xFF3B82F6); // Blue for info
|
||||
|
||||
// Interactive Colors
|
||||
static const Color buttonPrimary = Color(0xFF82C9BD); // Primary button (teal)
|
||||
static const Color buttonAccent = Color(0xFFF68D2D); // Accent button (orange)
|
||||
static const Color buttonSecondary = Color(0xFFE5E7EB); // Secondary button
|
||||
static const Color iconActive = Color(0xFF82C9BD); // Active icons (teal)
|
||||
static const Color iconInactive = Color(0xFF9CA3AF); // Inactive icons
|
||||
|
||||
// Chat Specific Colors
|
||||
static const Color chatBubbleStudent = Color(
|
||||
0xFF82C9BD,
|
||||
); // Student messages (teal)
|
||||
static const Color chatBubbleAI = Color(0xFFF3F4F6); // AI messages
|
||||
static const Color chatInputBackground = Color(
|
||||
0xFFF8F9FA,
|
||||
); // Input background
|
||||
static const Color chatSendButton = Color(0xFF82C9BD); // Send button (teal)
|
||||
|
||||
// Dark Mode Colors
|
||||
static const Color darkBackground = Color(0xFF1F2937); // Dark background
|
||||
static const Color darkSurface = Color(0xFF374151); // Dark surface
|
||||
static const Color darkTextPrimary = Color(0xFFF9FAFB); // Dark primary text
|
||||
static const Color darkTextSecondary = Color(
|
||||
0xFFD1D5DB,
|
||||
); // Dark secondary text
|
||||
|
||||
// Legacy compatibility (for existing code)
|
||||
@deprecated
|
||||
static const Color primaryBlue = primaryTeal; // Map old primaryBlue to new primaryTeal
|
||||
}
|
||||
412
lib/core/theme/app_theme.dart
Normal file
412
lib/core/theme/app_theme.dart
Normal file
@@ -0,0 +1,412 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'app_colors.dart';
|
||||
|
||||
/// Application Theme Configuration
|
||||
class AppTheme {
|
||||
static ThemeData get lightTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: AppColors.primaryBlue,
|
||||
brightness: Brightness.light,
|
||||
primary: AppColors.primaryBlue,
|
||||
secondary: AppColors.primaryTeal,
|
||||
surface: AppColors.surface,
|
||||
background: AppColors.background,
|
||||
error: AppColors.error,
|
||||
),
|
||||
|
||||
// App Bar Theme
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimary,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||
titleTextStyle: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
|
||||
// Card Theme
|
||||
cardTheme: CardThemeData(
|
||||
color: AppColors.cardBackground,
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.08),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
|
||||
// Elevated Button Theme
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.buttonPrimary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 2,
|
||||
shadowColor: AppColors.primaryBlue.withOpacity(0.3),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
|
||||
// Outlined Button Theme
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryBlue,
|
||||
side: BorderSide(color: AppColors.primaryBlue.withOpacity(0.3)),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||
textStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
|
||||
// Text Button Theme
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryBlue,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
textStyle: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
|
||||
// Input Field Theme
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: AppColors.surface,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: AppColors.primaryBlue.withOpacity(0.3)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: AppColors.primaryBlue.withOpacity(0.3)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: AppColors.error),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
hintStyle: const TextStyle(color: AppColors.textHint, fontSize: 14),
|
||||
labelStyle: const TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
// Text Field Theme
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
cursorColor: AppColors.primaryBlue,
|
||||
selectionColor: AppColors.primaryBlue.withOpacity(0.3),
|
||||
selectionHandleColor: AppColors.primaryBlue,
|
||||
),
|
||||
|
||||
// Text Theme
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
displaySmall: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
headlineLarge: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
headlineSmall: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
titleLarge: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
titleSmall: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
labelLarge: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
labelMedium: TextStyle(
|
||||
color: AppColors.textSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
labelSmall: TextStyle(
|
||||
color: AppColors.textHint,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom Navigation Bar Theme
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
backgroundColor: AppColors.surface,
|
||||
selectedItemColor: AppColors.primaryBlue,
|
||||
unselectedItemColor: AppColors.iconInactive,
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 8,
|
||||
selectedLabelStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
|
||||
// Floating Action Button Theme
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
|
||||
// Divider Theme
|
||||
dividerTheme: const DividerThemeData(
|
||||
color: AppColors.buttonSecondary,
|
||||
thickness: 1,
|
||||
space: 1,
|
||||
),
|
||||
|
||||
// Icon Theme
|
||||
iconTheme: const IconThemeData(color: AppColors.iconActive, size: 24),
|
||||
|
||||
// Progress Indicator Theme
|
||||
progressIndicatorTheme: const ProgressIndicatorThemeData(
|
||||
color: AppColors.primaryBlue,
|
||||
linearTrackColor: AppColors.buttonSecondary,
|
||||
circularTrackColor: AppColors.buttonSecondary,
|
||||
),
|
||||
|
||||
// Chip Theme
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: AppColors.buttonSecondary,
|
||||
selectedColor: AppColors.primaryBlue.withOpacity(0.1),
|
||||
disabledColor: AppColors.buttonSecondary.withOpacity(0.5),
|
||||
labelStyle: const TextStyle(color: AppColors.textPrimary),
|
||||
secondaryLabelStyle: const TextStyle(color: AppColors.textPrimary),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static ThemeData get darkTheme {
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: AppColors.primaryBlue,
|
||||
brightness: Brightness.dark,
|
||||
primary: AppColors.primaryBlue,
|
||||
secondary: AppColors.primaryTeal,
|
||||
surface: AppColors.darkSurface,
|
||||
background: AppColors.darkBackground,
|
||||
error: AppColors.error,
|
||||
),
|
||||
|
||||
// Dark mode specific overrides would go here
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: AppColors.darkSurface,
|
||||
foregroundColor: AppColors.darkTextPrimary,
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
titleTextStyle: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
|
||||
cardTheme: CardThemeData(
|
||||
color: AppColors.darkSurface,
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black.withOpacity(0.3),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
|
||||
// Input Field Theme for Dark Mode
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: AppColors.darkSurface,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: AppColors.primaryBlue.withOpacity(0.3)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: BorderSide(color: AppColors.primaryBlue.withOpacity(0.3)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: AppColors.error),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
hintStyle: const TextStyle(
|
||||
color: AppColors.darkTextSecondary,
|
||||
fontSize: 14,
|
||||
),
|
||||
labelStyle: const TextStyle(
|
||||
color: AppColors.darkTextSecondary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
// Text Field Theme for Dark Mode
|
||||
textSelectionTheme: TextSelectionThemeData(
|
||||
cursorColor: AppColors.primaryBlue,
|
||||
selectionColor: AppColors.primaryBlue.withOpacity(0.3),
|
||||
selectionHandleColor: AppColors.primaryBlue,
|
||||
),
|
||||
|
||||
textTheme: const TextTheme(
|
||||
displayLarge: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
displayMedium: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
displaySmall: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
headlineLarge: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
headlineMedium: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
headlineSmall: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
titleLarge: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
titleMedium: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
titleSmall: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
bodyLarge: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
bodyMedium: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
bodySmall: TextStyle(
|
||||
color: AppColors.darkTextSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
labelLarge: TextStyle(
|
||||
color: AppColors.darkTextPrimary,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
labelMedium: TextStyle(
|
||||
color: AppColors.darkTextSecondary,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
labelSmall: TextStyle(
|
||||
color: AppColors.darkTextSecondary,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
352
lib/features/auth/presentation/pages/login_page.dart
Normal file
352
lib/features/auth/presentation/pages/login_page.dart
Normal file
@@ -0,0 +1,352 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../l10n/app_localizations.dart';
|
||||
import '../../../../core/services/auth_service.dart';
|
||||
import '../../../../shared/presentation/widgets/custom_notification.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
bool _obscurePassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleLogin() async {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Get email and password from controllers
|
||||
final email = _emailController.text.trim();
|
||||
final password = _passwordController.text.trim();
|
||||
|
||||
print('DEBUG: Iniciando processo de login para: $email');
|
||||
|
||||
// Attempt login with Firebase
|
||||
await AuthService.signInWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
print('DEBUG: Login Firebase bem-sucedido, navegando para dashboard');
|
||||
|
||||
// Navigate to student dashboard after successful login
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
// Show success message
|
||||
NotificationHelper.showSuccess(
|
||||
context,
|
||||
message: 'Login realizado com sucesso!',
|
||||
);
|
||||
|
||||
context.go('/student-dashboard');
|
||||
}
|
||||
} catch (e) {
|
||||
print('DEBUG: Erro no login: $e');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Show error message
|
||||
NotificationHelper.showError(
|
||||
context,
|
||||
message: e.toString().replaceAll('Exception: ', ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color(0xFFF8F9FA),
|
||||
Color.fromRGBO(130, 201, 189, 0.1),
|
||||
Color.fromRGBO(246, 141, 45, 0.05),
|
||||
Color(0xFFF8F9FA),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
|
||||
// Logo/Title
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10.0,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'EPVC',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
foreground: Paint()
|
||||
..shader = LinearGradient(
|
||||
colors: [
|
||||
const Color(0xFF82C9BD),
|
||||
const Color(0xFFF68D2D),
|
||||
],
|
||||
).createShader(Rect.fromLTWH(0, 0, 200, 20)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Escola Profissional de Vila do Conde',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: const Color(0xFF2D3748),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Login form
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10.0,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Entrar',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF2D3748),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Email field
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
style: const TextStyle(color: Color(0xFF2D3748)),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
labelStyle: const TextStyle(
|
||||
color: Color(0xFF2D3748),
|
||||
),
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xFF718096),
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.email,
|
||||
color: Color(0xFF82C9BD),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFE2E8F0),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF82C9BD),
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFF8F9FA),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Email é obrigatório';
|
||||
}
|
||||
if (!value.contains('@')) {
|
||||
return 'Email inválido';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Password field
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
style: const TextStyle(color: Color(0xFF2D3748)),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Palavra-passe',
|
||||
labelStyle: const TextStyle(
|
||||
color: Color(0xFF2D3748),
|
||||
),
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xFF718096),
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.lock,
|
||||
color: Color(0xFF82C9BD),
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: const Color(0xFF82C9BD),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFE2E8F0),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF82C9BD),
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFF8F9FA),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Palavra-passe é obrigatória';
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return 'Palavra-passe muito curta';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Login button
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleLogin,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF82C9BD),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'Entrar',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Signup link
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.go('/signup');
|
||||
},
|
||||
child: Text(
|
||||
'Não tem conta? Criar aqui',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF82C9BD),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
407
lib/features/auth/presentation/pages/role_selection_page.dart
Normal file
407
lib/features/auth/presentation/pages/role_selection_page.dart
Normal file
@@ -0,0 +1,407 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../l10n/app_localizations.dart';
|
||||
|
||||
class RoleSelectionPage extends StatefulWidget {
|
||||
const RoleSelectionPage({super.key});
|
||||
|
||||
@override
|
||||
State<RoleSelectionPage> createState() => _RoleSelectionPageState();
|
||||
}
|
||||
|
||||
class _RoleSelectionPageState extends State<RoleSelectionPage> {
|
||||
String? _selectedRole;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
AppColors.background,
|
||||
AppColors.primaryBlue.withOpacity(0.05),
|
||||
AppColors.gradientStart.withOpacity(0.1),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Animated background particles
|
||||
...List.generate(20, (index) => _buildParticle(index)),
|
||||
|
||||
// Main content
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
|
||||
// Logo and title
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
AppColors.gradientStart,
|
||||
AppColors.gradientEnd,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(25),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryBlue.withOpacity(
|
||||
0.3,
|
||||
),
|
||||
blurRadius: 25,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.school,
|
||||
size: 50,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
curve: Curves.elasticOut,
|
||||
)
|
||||
.then()
|
||||
.shimmer(
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
Text(
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
style: Theme.of(context).textTheme.headlineLarge
|
||||
?.copyWith(
|
||||
color: AppColors.textPrimary,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
delay: const Duration(milliseconds: 200),
|
||||
)
|
||||
.slideY(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
delay: const Duration(milliseconds: 200),
|
||||
begin: -0.3,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
ShaderMask(
|
||||
shaderCallback: (bounds) =>
|
||||
const LinearGradient(
|
||||
colors: [
|
||||
AppColors.primaryTeal,
|
||||
AppColors.primaryOrange,
|
||||
],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
).createShader(bounds),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.schoolName,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
delay: const Duration(milliseconds: 400),
|
||||
)
|
||||
.slideY(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
delay: const Duration(milliseconds: 400),
|
||||
begin: -0.2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Role selection title
|
||||
Text(
|
||||
'Quem é você?',
|
||||
style: Theme.of(context).textTheme.headlineMedium
|
||||
?.copyWith(
|
||||
color: AppColors.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
delay: const Duration(milliseconds: 600),
|
||||
)
|
||||
.slideY(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
delay: const Duration(milliseconds: 600),
|
||||
begin: -0.3,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
'Selecione o seu papel para continuar',
|
||||
style: Theme.of(context).textTheme.bodyLarge
|
||||
?.copyWith(color: AppColors.primaryOrange),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
delay: const Duration(milliseconds: 800),
|
||||
)
|
||||
.slideY(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
delay: const Duration(milliseconds: 800),
|
||||
begin: -0.2,
|
||||
),
|
||||
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// Role cards
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildRoleCard(
|
||||
context,
|
||||
'Aluno',
|
||||
Icons.school_outlined,
|
||||
'student',
|
||||
AppColors.gradientStart,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _buildRoleCard(
|
||||
context,
|
||||
'Professor',
|
||||
Icons.person_outline,
|
||||
'teacher',
|
||||
AppColors.gradientEnd,
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
delay: const Duration(milliseconds: 1000),
|
||||
)
|
||||
.slideY(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
delay: const Duration(milliseconds: 1000),
|
||||
begin: 0.3,
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Continue button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
child: ElevatedButton(
|
||||
onPressed: _selectedRole != null
|
||||
? _handleContinue
|
||||
: null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 4,
|
||||
shadowColor: AppColors.primaryBlue.withOpacity(
|
||||
0.3,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
child: _selectedRole != null
|
||||
? const Text(
|
||||
'Continuar',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
)
|
||||
: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
delay: const Duration(milliseconds: 1200),
|
||||
)
|
||||
.slideY(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
delay: const Duration(milliseconds: 1200),
|
||||
begin: 0.3,
|
||||
),
|
||||
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRoleCard(
|
||||
BuildContext context,
|
||||
String title,
|
||||
IconData icon,
|
||||
String role,
|
||||
Color gradientColor,
|
||||
) {
|
||||
final isSelected = _selectedRole == role;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _selectedRole = role),
|
||||
child:
|
||||
Container(
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
gradient: isSelected
|
||||
? LinearGradient(
|
||||
colors: [
|
||||
gradientColor,
|
||||
gradientColor.withOpacity(0.8),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
)
|
||||
: null,
|
||||
color: isSelected ? null : Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? gradientColor
|
||||
: AppColors.primaryBlue.withOpacity(0.2),
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isSelected
|
||||
? gradientColor.withOpacity(0.3)
|
||||
: Colors.black.withOpacity(0.1),
|
||||
blurRadius: isSelected ? 15 : 8,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 48,
|
||||
color: isSelected ? Colors.white : AppColors.primaryBlue,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: AppColors.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
begin: const Offset(1.0, 1.0),
|
||||
end: const Offset(1.05, 1.05),
|
||||
)
|
||||
.then()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
begin: const Offset(1.05, 1.05),
|
||||
end: const Offset(1.0, 1.0),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildParticle(int index) {
|
||||
final random = index * 137.5;
|
||||
return Positioned(
|
||||
top: (random % 300) + 50,
|
||||
left: (random % 200) + 50,
|
||||
child:
|
||||
Container(
|
||||
width: 4,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.gradientStart.withOpacity(0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.moveY(
|
||||
duration: Duration(milliseconds: 3000 + (index * 200)),
|
||||
begin: 0.0,
|
||||
end: -200.0,
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
delay: Duration(milliseconds: index * 100),
|
||||
)
|
||||
.then()
|
||||
.fadeOut(duration: const Duration(milliseconds: 1000)),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleContinue() {
|
||||
if (_selectedRole != null) {
|
||||
// Store the selected role and navigate to signup
|
||||
// TODO: Store role in shared preferences or state management
|
||||
context.go('/signup?role=$_selectedRole');
|
||||
}
|
||||
}
|
||||
}
|
||||
352
lib/features/auth/presentation/pages/signup_page.dart
Normal file
352
lib/features/auth/presentation/pages/signup_page.dart
Normal file
@@ -0,0 +1,352 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../l10n/app_localizations.dart';
|
||||
import '../../../../core/services/auth_service.dart';
|
||||
import '../../../../shared/presentation/widgets/custom_notification.dart';
|
||||
|
||||
class SignupPage extends StatefulWidget {
|
||||
const SignupPage({super.key});
|
||||
|
||||
@override
|
||||
State<SignupPage> createState() => _SignupPageState();
|
||||
}
|
||||
|
||||
class _SignupPageState extends State<SignupPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
bool _obscurePassword = true;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _handleSignup() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
});
|
||||
|
||||
try {
|
||||
// Get email and password from controllers
|
||||
final email = _emailController.text.trim();
|
||||
final password = _passwordController.text.trim();
|
||||
|
||||
print('DEBUG: Iniciando processo de signup para: $email');
|
||||
|
||||
// Attempt signup with Firebase
|
||||
await AuthService.signUpWithEmailAndPassword(
|
||||
email: email,
|
||||
password: password,
|
||||
);
|
||||
|
||||
print('DEBUG: Signup Firebase bem-sucedido, navegando para dashboard');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
// Show success message
|
||||
NotificationHelper.showSuccess(
|
||||
context,
|
||||
message: 'Conta criada com sucesso!',
|
||||
);
|
||||
|
||||
// Navigate to student dashboard after successful signup
|
||||
context.go('/student-dashboard');
|
||||
}
|
||||
} catch (e) {
|
||||
print('DEBUG: Erro no signup: $e');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
|
||||
// Show error message
|
||||
NotificationHelper.showError(
|
||||
context,
|
||||
message: e.toString().replaceAll('Exception: ', ''),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
Color(0xFFF8F9FA),
|
||||
Color.fromRGBO(130, 201, 189, 0.1),
|
||||
Color.fromRGBO(246, 141, 45, 0.05),
|
||||
Color(0xFFF8F9FA),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 60),
|
||||
|
||||
// Logo/Title
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10.0,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
'EPVC',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
foreground: Paint()
|
||||
..shader = LinearGradient(
|
||||
colors: [
|
||||
const Color(0xFF82C9BD),
|
||||
const Color(0xFFF68D2D),
|
||||
],
|
||||
).createShader(Rect.fromLTWH(0, 0, 200, 20)),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Escola Profissional de Vila do Conde',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: const Color(0xFF2D3748),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Signup form
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 10.0,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'Criar Conta',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF2D3748),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Email field
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
style: const TextStyle(color: Color(0xFF2D3748)),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email',
|
||||
labelStyle: const TextStyle(
|
||||
color: Color(0xFF2D3748),
|
||||
),
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xFF718096),
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.email,
|
||||
color: Color(0xFF82C9BD),
|
||||
),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFE2E8F0),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF82C9BD),
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFF8F9FA),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Email é obrigatório';
|
||||
}
|
||||
if (!value.contains('@')) {
|
||||
return 'Email inválido';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Password field
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
style: const TextStyle(color: Color(0xFF2D3748)),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Palavra-passe',
|
||||
labelStyle: const TextStyle(
|
||||
color: Color(0xFF2D3748),
|
||||
),
|
||||
hintStyle: const TextStyle(
|
||||
color: Color(0xFF718096),
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
Icons.lock,
|
||||
color: Color(0xFF82C9BD),
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? Icons.visibility
|
||||
: Icons.visibility_off,
|
||||
color: const Color(0xFF82C9BD),
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_obscurePassword = !_obscurePassword;
|
||||
});
|
||||
},
|
||||
),
|
||||
border: InputBorder.none,
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFFE2E8F0),
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
borderSide: const BorderSide(
|
||||
color: Color(0xFF82C9BD),
|
||||
),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFF8F9FA),
|
||||
),
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Palavra-passe é obrigatória';
|
||||
}
|
||||
if (value.length < 6) {
|
||||
return 'Palavra-passe muito curta';
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Signup button
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleSignup,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF82C9BD),
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
),
|
||||
elevation: 2,
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor:
|
||||
AlwaysStoppedAnimation<Color>(
|
||||
Colors.white,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
'Criar Conta',
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Login link
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
context.go('/login');
|
||||
},
|
||||
child: Text(
|
||||
'Já tem conta? Entrar aqui',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF82C9BD),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
).animate().fadeIn(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/services/auth_service.dart';
|
||||
import '../widgets/progress_hero_widget.dart';
|
||||
import '../widgets/quick_access_widget.dart';
|
||||
import '../widgets/profile_section_widget.dart';
|
||||
|
||||
class StudentDashboardPage extends StatefulWidget {
|
||||
const StudentDashboardPage({super.key});
|
||||
|
||||
@override
|
||||
State<StudentDashboardPage> createState() => _StudentDashboardPageState();
|
||||
}
|
||||
|
||||
class _StudentDashboardPageState extends State<StudentDashboardPage> {
|
||||
String _userName = 'Estudante';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadUserData();
|
||||
}
|
||||
|
||||
void _loadUserData() {
|
||||
final user = AuthService.currentUser;
|
||||
if (user != null) {
|
||||
setState(() {
|
||||
_userName = user.displayName ?? 'Estudante';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xFF82C9BD),
|
||||
Color(0xFF7BA89C),
|
||||
Color(0xFFF68D2D),
|
||||
Color(0xFFF8F9FA),
|
||||
],
|
||||
stops: [0.0, 0.2, 0.6, 1.0],
|
||||
),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header with logout
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Bem-vindo, $_userName!',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Seu progresso de estudos',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.logout, color: Colors.white),
|
||||
onPressed: () async {
|
||||
await AuthService.signOut();
|
||||
if (mounted) {
|
||||
context.go('/login');
|
||||
}
|
||||
},
|
||||
tooltip: 'Sair',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Progress Hero Section (Priority 1)
|
||||
ProgressHeroWidget(userName: _userName),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Quick Access Section (Priority 2)
|
||||
const QuickAccessWidget(),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Profile Section (Priority 3)
|
||||
const ProfileSectionWidget(),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
|
||||
class TeacherDashboardPage extends StatelessWidget {
|
||||
const TeacherDashboardPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('Teacher Dashboard'),
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimary,
|
||||
elevation: 0,
|
||||
),
|
||||
body: const Center(
|
||||
child: Text(
|
||||
'Teacher Dashboard - Coming Soon',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import '../../../../core/services/auth_service.dart';
|
||||
|
||||
/// Profile section with user info and achievements
|
||||
class ProfileSectionWidget extends StatelessWidget {
|
||||
const ProfileSectionWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final user = AuthService.currentUser;
|
||||
final userName = user?.displayName ?? 'Estudante';
|
||||
final userEmail = user?.email ?? '';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(top: 24),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Profile Header
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF82C9BD), Color(0xFF6BA8A0)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
userName,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF2D3748),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
userEmail,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF718096),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
if (userEmail.length > 20) ...[
|
||||
const SizedBox(width: 8),
|
||||
const Icon(
|
||||
Icons.more_horiz,
|
||||
color: Color(0xFF718096),
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF68D2D).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.settings,
|
||||
color: Color(0xFFF68D2D),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Achievements
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.emoji_events,
|
||||
color: Color(0xFFF68D2D),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Conquistas',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF2D3748),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Achievement Badges
|
||||
Row(
|
||||
children: [
|
||||
_buildAchievementBadge(
|
||||
icon: Icons.local_fire_department,
|
||||
label: '7 dias',
|
||||
color: const Color(0xFFF68D2D),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildAchievementBadge(
|
||||
icon: Icons.school,
|
||||
label: '3 conceitos',
|
||||
color: const Color(0xFF82C9BD),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildAchievementBadge(
|
||||
icon: Icons.speed,
|
||||
label: 'Rápido',
|
||||
color: const Color(0xFF6BA8A0),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildAchievementBadge(
|
||||
icon: Icons.star,
|
||||
label: '100%',
|
||||
color: const Color(0xFF4CAF50),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Recent Activity Summary
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8F9FA),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.trending_up,
|
||||
color: Color(0xFF82C9BD),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Ótimo progresso!',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF2D3748),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Você está 15% acima da média esta semana',
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF718096),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.slideY(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
curve: Curves.easeOut,
|
||||
)
|
||||
.then(delay: const Duration(milliseconds: 400));
|
||||
}
|
||||
|
||||
Widget _buildAchievementBadge({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required Color color,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: color.withOpacity(0.3), width: 1),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 16),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
/// Progress tracking hero section for student dashboard
|
||||
class ProgressHeroWidget extends StatelessWidget {
|
||||
final String userName;
|
||||
final double overallProgress;
|
||||
final List<String> masteredConcepts;
|
||||
final int studyTimeMinutes;
|
||||
final int streakDays;
|
||||
|
||||
const ProgressHeroWidget({
|
||||
super.key,
|
||||
required this.userName,
|
||||
this.overallProgress = 0.65,
|
||||
this.masteredConcepts = const [
|
||||
'Fundamentos de Programação',
|
||||
'Algoritmos Básicos',
|
||||
'Estruturas de Dados',
|
||||
],
|
||||
this.studyTimeMinutes = 245,
|
||||
this.streakDays = 7,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 24),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Seu Progresso',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF2D3748),
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Continue assim, $userName!',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF718096),
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF82C9BD),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.local_fire_department,
|
||||
color: Colors.white,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'$streakDays dias',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Main Progress Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
const Color(0xFF82C9BD),
|
||||
const Color(0xFF6BA8A0),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Overall Progress
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Progresso Geral',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${(overallProgress * 100).toInt()}%',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Progress Bar
|
||||
Container(
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: FractionallySizedBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
widthFactor: overallProgress,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Colors.white, Color(0xFFF8F9FA)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Stats Grid
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
icon: Icons.access_time,
|
||||
value: '${(studyTimeMinutes / 60).toStringAsFixed(1)}h',
|
||||
label: 'Tempo de Estudo',
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
icon: Icons.emoji_events,
|
||||
value: '${masteredConcepts.length}',
|
||||
label: 'Conceitos Dominados',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
).animate().scale(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.elasticOut,
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Mastered Concepts
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.school,
|
||||
color: Color(0xFFF68D2D),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text(
|
||||
'Conceitos Dominados',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF2D3748),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
...masteredConcepts.map((concept) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 8,
|
||||
height: 8,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFF82C9BD),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
concept,
|
||||
style: const TextStyle(
|
||||
color: Color(0xFF4A5568),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Icon(
|
||||
Icons.check_circle,
|
||||
color: Color(0xFF82C9BD),
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
).animate().slideX(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
curve: Curves.easeOut,
|
||||
).then(delay: const Duration(milliseconds: 200)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCard({
|
||||
required IconData icon,
|
||||
required String value,
|
||||
required String label,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: Colors.white.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(icon, color: Colors.white, size: 24),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Quick access cards for Tutor IA and Quiz with fixed overflow
|
||||
class QuickAccessWidget extends StatelessWidget {
|
||||
const QuickAccessWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Acesso Rápido',
|
||||
style: TextStyle(
|
||||
color: const Color(0xFF2D3748),
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
// Tutor IA Card (Primary)
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: _buildTutorIACard(context),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
// Quiz Card (Secondary)
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: _buildQuizCard(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
).animate().slideY(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
curve: Curves.easeOut,
|
||||
).then(delay: const Duration(milliseconds: 200));
|
||||
}
|
||||
|
||||
Widget _buildTutorIACard(BuildContext context) {
|
||||
return Container(
|
||||
height: 135,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
const Color(0xFF82C9BD),
|
||||
const Color(0xFF6BA8A0),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color(0xFF82C9BD).withOpacity(0.3),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => context.go('/tutor'),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.psychology,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF68D2D),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Text(
|
||||
'NOVO',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Tutor IA',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Assistente de estudos',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
).animate().scale(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.elasticOut,
|
||||
).then(delay: const Duration(milliseconds: 100));
|
||||
}
|
||||
|
||||
Widget _buildQuizCard(BuildContext context) {
|
||||
return Container(
|
||||
height: 135,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFE2E8F0),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
onTap: () => context.go('/quiz'),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF68D2D).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.quiz,
|
||||
color: Color(0xFFF68D2D),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Quiz',
|
||||
style: TextStyle(
|
||||
color: Color(0xFF2D3748),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Teste conhecimentos',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Color(0xFF718096),
|
||||
fontSize: 12,
|
||||
height: 1.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
).animate().scale(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
curve: Curves.elasticOut,
|
||||
).then(delay: const Duration(milliseconds: 200));
|
||||
}
|
||||
}
|
||||
29
lib/features/profile/presentation/pages/profile_page.dart
Normal file
29
lib/features/profile/presentation/pages/profile_page.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
|
||||
class ProfilePage extends StatelessWidget {
|
||||
const ProfilePage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('Profile'),
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimary,
|
||||
elevation: 0,
|
||||
),
|
||||
body: const Center(
|
||||
child: Text(
|
||||
'Profile Page - Coming Soon',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
29
lib/features/quiz/presentation/pages/quiz_list_page.dart
Normal file
29
lib/features/quiz/presentation/pages/quiz_list_page.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
|
||||
class QuizListPage extends StatelessWidget {
|
||||
const QuizListPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('Quizzes'),
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimary,
|
||||
elevation: 0,
|
||||
),
|
||||
body: const Center(
|
||||
child: Text(
|
||||
'Quiz List - Coming Soon',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
35
lib/features/quiz/presentation/pages/quiz_page.dart
Normal file
35
lib/features/quiz/presentation/pages/quiz_page.dart
Normal file
@@ -0,0 +1,35 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
|
||||
class QuizPage extends StatelessWidget {
|
||||
final String quizId;
|
||||
|
||||
const QuizPage({
|
||||
super.key,
|
||||
required this.quizId,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
title: Text('Quiz $quizId'),
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimary,
|
||||
elevation: 0,
|
||||
),
|
||||
body: Center(
|
||||
child: Text(
|
||||
'Quiz Page - ID: $quizId\nComing Soon',
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
360
lib/features/splash/presentation/pages/splash_page.dart
Normal file
360
lib/features/splash/presentation/pages/splash_page.dart
Normal file
@@ -0,0 +1,360 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../l10n/app_localizations.dart';
|
||||
|
||||
class SplashPage extends StatefulWidget {
|
||||
const SplashPage({super.key});
|
||||
|
||||
@override
|
||||
State<SplashPage> createState() => _SplashPageState();
|
||||
}
|
||||
|
||||
class _SplashPageState extends State<SplashPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_navigateToRoleSelection();
|
||||
}
|
||||
|
||||
void _navigateToRoleSelection() async {
|
||||
await Future.delayed(const Duration(seconds: 3));
|
||||
if (mounted) {
|
||||
context.go('/role-selection');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
AppColors.background,
|
||||
AppColors.primaryTeal.withOpacity(0.05),
|
||||
AppColors.primaryOrange.withOpacity(0.03),
|
||||
AppColors.background,
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
// Animated background particles
|
||||
...List.generate(25, (index) => _buildParticle(index)),
|
||||
|
||||
// Main content
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Modern logo with gradient animation
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Outer ring animation
|
||||
Container(
|
||||
width: 160,
|
||||
height: 160,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColors.primaryTeal.withOpacity(0.2),
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
curve: Curves.easeInOut,
|
||||
begin: const Offset(0.8, 0.8),
|
||||
end: const Offset(1.2, 1.2),
|
||||
)
|
||||
.then()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
curve: Curves.easeInOut,
|
||||
begin: const Offset(1.2, 1.2),
|
||||
end: const Offset(0.8, 0.8),
|
||||
),
|
||||
|
||||
// Middle ring animation
|
||||
Container(
|
||||
width: 120,
|
||||
height: 120,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColors.primaryOrange.withOpacity(0.3),
|
||||
width: 3,
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
curve: Curves.easeInOut,
|
||||
begin: const Offset(1.0, 1.0),
|
||||
end: const Offset(1.1, 1.1),
|
||||
)
|
||||
.then()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 1500),
|
||||
curve: Curves.easeInOut,
|
||||
begin: const Offset(1.1, 1.1),
|
||||
end: const Offset(1.0, 1.0),
|
||||
),
|
||||
|
||||
// Main logo container
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
AppColors.primaryTeal,
|
||||
AppColors.primaryOrange,
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryTeal.withOpacity(0.3),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
BoxShadow(
|
||||
color: AppColors.primaryOrange.withOpacity(
|
||||
0.2,
|
||||
),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.school,
|
||||
size: 40,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
curve: Curves.elasticOut,
|
||||
)
|
||||
.then()
|
||||
.shimmer(
|
||||
duration: const Duration(milliseconds: 2500),
|
||||
color: Colors.white.withOpacity(0.4),
|
||||
)
|
||||
.then()
|
||||
.rotate(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
curve: Curves.easeInOut,
|
||||
begin: 0.0,
|
||||
end: 0.05,
|
||||
)
|
||||
.then()
|
||||
.rotate(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
curve: Curves.easeInOut,
|
||||
begin: 0.05,
|
||||
end: 0.0,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// School name with gradient
|
||||
ShaderMask(
|
||||
shaderCallback: (bounds) => const LinearGradient(
|
||||
colors: [
|
||||
AppColors.primaryTeal,
|
||||
AppColors.primaryOrange,
|
||||
],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
).createShader(bounds),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
style: Theme.of(context).textTheme.headlineMedium
|
||||
?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
delay: const Duration(milliseconds: 500),
|
||||
)
|
||||
.slideY(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
delay: const Duration(milliseconds: 500),
|
||||
begin: 0.3,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// School name subtitle
|
||||
ShaderMask(
|
||||
shaderCallback: (bounds) => LinearGradient(
|
||||
colors: [
|
||||
AppColors.primaryOrange.withOpacity(0.8),
|
||||
AppColors.primaryTeal.withOpacity(0.8),
|
||||
],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
).createShader(bounds),
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.schoolName,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
delay: const Duration(milliseconds: 700),
|
||||
)
|
||||
.slideY(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
delay: const Duration(milliseconds: 700),
|
||||
begin: 0.2,
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Modern loading indicator
|
||||
Column(
|
||||
children: [
|
||||
// Loading dots
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildLoadingDot(0),
|
||||
const SizedBox(width: 8),
|
||||
_buildLoadingDot(1),
|
||||
const SizedBox(width: 8),
|
||||
_buildLoadingDot(2),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Loading text
|
||||
Text(
|
||||
'A preparar a sua experiência...',
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(
|
||||
color: AppColors.primaryOrange,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 800),
|
||||
delay: const Duration(milliseconds: 1200),
|
||||
)
|
||||
.then()
|
||||
.shimmer(
|
||||
duration: const Duration(milliseconds: 2000),
|
||||
color: AppColors.primaryTeal.withOpacity(0.3),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingDot(int index) {
|
||||
return Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [AppColors.primaryTeal, AppColors.primaryOrange],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 600),
|
||||
delay: Duration(milliseconds: index * 200),
|
||||
curve: Curves.elasticOut,
|
||||
begin: const Offset(0.0, 0.0),
|
||||
end: const Offset(1.0, 1.0),
|
||||
)
|
||||
.then()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
delay: Duration(milliseconds: 800 + index * 200),
|
||||
begin: const Offset(1.0, 1.0),
|
||||
end: const Offset(0.8, 0.8),
|
||||
)
|
||||
.then()
|
||||
.scale(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
begin: const Offset(0.8, 0.8),
|
||||
end: const Offset(1.0, 1.0),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildParticle(int index) {
|
||||
final random = index * 137.5;
|
||||
return Positioned(
|
||||
top: (random % 400) + 50,
|
||||
left: (random % 300) + 50,
|
||||
child:
|
||||
Container(
|
||||
width: 2,
|
||||
height: 2,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppColors.primaryTeal.withOpacity(0.3),
|
||||
AppColors.primaryOrange.withOpacity(0.2),
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
)
|
||||
.animate()
|
||||
.moveY(
|
||||
duration: Duration(milliseconds: 4000 + (index * 200)),
|
||||
begin: 0.0,
|
||||
end: -200.0,
|
||||
curve: Curves.easeInOut,
|
||||
)
|
||||
.fadeIn(
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
delay: Duration(milliseconds: index * 150),
|
||||
)
|
||||
.then()
|
||||
.fadeOut(duration: const Duration(milliseconds: 1000)),
|
||||
);
|
||||
}
|
||||
}
|
||||
29
lib/features/tutor/presentation/pages/tutor_chat_page.dart
Normal file
29
lib/features/tutor/presentation/pages/tutor_chat_page.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
|
||||
class TutorChatPage extends StatelessWidget {
|
||||
const TutorChatPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('AI Tutor'),
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimary,
|
||||
elevation: 0,
|
||||
),
|
||||
body: const Center(
|
||||
child: Text(
|
||||
'AI Tutor Chat - Coming Soon',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
116
lib/l10n/app_en.arb
Normal file
116
lib/l10n/app_en.arb
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"@@last_modified": "2024-05-06T21:26:00.000Z",
|
||||
"appTitle": "AI Study Assistant",
|
||||
"@appTitle": {
|
||||
"description": "Title of the application"
|
||||
},
|
||||
"schoolName": "Escola Profissional de Vila do Conde",
|
||||
"@schoolName": {
|
||||
"description": "Name of the school"
|
||||
},
|
||||
"welcomeBack": "Welcome Back",
|
||||
"@welcomeBack": {
|
||||
"description": "Welcome back message on login screen"
|
||||
},
|
||||
"signInToContinue": "Sign in to continue learning",
|
||||
"@signInToContinue": {
|
||||
"description": "Subtitle on login screen"
|
||||
},
|
||||
"email": "Email",
|
||||
"@email": {
|
||||
"description": "Email field label"
|
||||
},
|
||||
"password": "Password",
|
||||
"@password": {
|
||||
"description": "Password field label"
|
||||
},
|
||||
"enterYourEmail": "Enter your email",
|
||||
"@enterYourEmail": {
|
||||
"description": "Email field hint"
|
||||
},
|
||||
"enterYourPassword": "Enter your password",
|
||||
"@enterYourPassword": {
|
||||
"description": "Password field hint"
|
||||
},
|
||||
"signIn": "Sign In",
|
||||
"@signIn": {
|
||||
"description": "Sign in button text"
|
||||
},
|
||||
"dontHaveAccount": "Don't have an account? ",
|
||||
"@dontHaveAccount": {
|
||||
"description": "Sign up prompt text"
|
||||
},
|
||||
"signUp": "Sign Up",
|
||||
"@signUp": {
|
||||
"description": "Sign up link text"
|
||||
},
|
||||
"createAccount": "Create Account",
|
||||
"@createAccount": {
|
||||
"description": "Create account title"
|
||||
},
|
||||
"joinOurCommunity": "Join our learning community",
|
||||
"@joinOurCommunity": {
|
||||
"description": "Sign up subtitle"
|
||||
},
|
||||
"confirmPassword": "Confirm Password",
|
||||
"@confirmPassword": {
|
||||
"description": "Confirm password field label"
|
||||
},
|
||||
"confirmYourPassword": "Confirm your password",
|
||||
"@confirmYourPassword": {
|
||||
"description": "Confirm password field hint"
|
||||
},
|
||||
"signUpButton": "Sign Up",
|
||||
"@signUpButton": {
|
||||
"description": "Sign up button text"
|
||||
},
|
||||
"alreadyHaveAccount": "Already have an account? ",
|
||||
"@alreadyHaveAccount": {
|
||||
"description": "Login prompt text"
|
||||
},
|
||||
"login": "Login",
|
||||
"@login": {
|
||||
"description": "Login link text"
|
||||
},
|
||||
"studentDashboard": "Student Dashboard",
|
||||
"@studentDashboard": {
|
||||
"description": "Student dashboard title"
|
||||
},
|
||||
"teacherDashboard": "Teacher Dashboard",
|
||||
"@teacherDashboard": {
|
||||
"description": "Teacher dashboard title"
|
||||
},
|
||||
"aiTutor": "AI Tutor",
|
||||
"@aiTutor": {
|
||||
"description": "AI Tutor title"
|
||||
},
|
||||
"quizzes": "Quizzes",
|
||||
"@quizzes": {
|
||||
"description": "Quizzes title"
|
||||
},
|
||||
"profile": "Profile",
|
||||
"@profile": {
|
||||
"description": "Profile title"
|
||||
},
|
||||
"pageNotFound": "Page Not Found",
|
||||
"@pageNotFound": {
|
||||
"description": "Page not found title"
|
||||
},
|
||||
"loading": "Loading...",
|
||||
"@loading": {
|
||||
"description": "Loading message"
|
||||
},
|
||||
"error": {
|
||||
"pleaseEnterEmail": "Please enter your email",
|
||||
"pleaseEnterValidEmail": "Please enter a valid email",
|
||||
"pleaseEnterPassword": "Please enter your password",
|
||||
"passwordTooShort": "Password must be at least 6 characters",
|
||||
"passwordsDoNotMatch": "Passwords do not match",
|
||||
"loginFailed": "Login failed",
|
||||
"signupFailed": "Sign up failed"
|
||||
},
|
||||
"@@error": {
|
||||
"description": "Error messages"
|
||||
}
|
||||
}
|
||||
82
lib/l10n/app_localizations.dart
Normal file
82
lib/l10n/app_localizations.dart
Normal file
@@ -0,0 +1,82 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppLocalizations {
|
||||
final Locale locale;
|
||||
|
||||
AppLocalizations(this.locale);
|
||||
|
||||
static AppLocalizations? of(BuildContext context) {
|
||||
return Localizations.of<AppLocalizations>(context, AppLocalizations);
|
||||
}
|
||||
|
||||
static const LocalizationsDelegate<AppLocalizations> delegate =
|
||||
_AppLocalizationsDelegate();
|
||||
|
||||
static const List<Locale> supportedLocales = [
|
||||
Locale('pt', 'PT'), // Portuguese (Portugal)
|
||||
Locale('en', 'US'), // English (United States)
|
||||
];
|
||||
|
||||
static const Locale fallbackLocale = Locale('pt', 'PT');
|
||||
|
||||
// App
|
||||
String get appTitle => 'Assistente de Estudo IA';
|
||||
String get schoolName => 'Escola Profissional de Vila do Conde';
|
||||
|
||||
// Login
|
||||
String get welcomeBack => 'Bem-vindo de volta';
|
||||
String get signInToContinue => 'Inicie sessão para continuar a aprender';
|
||||
String get email => 'Email';
|
||||
String get password => 'Palavra-passe';
|
||||
String get enterYourEmail => 'Introduza o seu email';
|
||||
String get enterYourPassword => 'Introduza a sua palavra-passe';
|
||||
String get signIn => 'Iniciar Sessão';
|
||||
String get dontHaveAccount => 'Não tem uma conta? ';
|
||||
String get signUp => 'Registar-se';
|
||||
|
||||
// Sign Up
|
||||
String get createAccount => 'Criar Conta';
|
||||
String get joinOurCommunity => 'Junte-se à nossa comunidade de aprendizagem';
|
||||
String get confirmPassword => 'Confirmar palavra-passe';
|
||||
String get confirmYourPassword => 'Confirme a sua palavra-passe';
|
||||
String get signUpButton => 'Registar';
|
||||
String get alreadyHaveAccount => 'Já tem uma conta? ';
|
||||
String get login => 'Iniciar Sessão';
|
||||
|
||||
// Dashboard
|
||||
String get studentDashboard => 'Painel do Aluno';
|
||||
String get teacherDashboard => 'Painel do Professor';
|
||||
String get aiTutor => 'Tutor IA';
|
||||
String get quizzes => 'Questionários';
|
||||
String get profile => 'Perfil';
|
||||
|
||||
// General
|
||||
String get pageNotFound => 'Página Não Encontrada';
|
||||
String get loading => 'A carregar...';
|
||||
|
||||
// Error messages
|
||||
String get pleaseEnterEmail => 'Por favor, introduza o seu email';
|
||||
String get pleaseEnterValidEmail => 'Por favor, introduza um email válido';
|
||||
String get pleaseEnterPassword => 'Por favor, introduza a sua palavra-passe';
|
||||
String get passwordTooShort => 'A palavra-passe deve ter pelo menos 6 caracteres';
|
||||
String get passwordsDoNotMatch => 'As palavras-passe não coincidem';
|
||||
String get loginFailed => 'Falha no início de sessão';
|
||||
String get signupFailed => 'Falha no registo';
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
|
||||
const _AppLocalizationsDelegate();
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) {
|
||||
return ['pt', 'en'].contains(locale.languageCode);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AppLocalizations> load(Locale locale) async {
|
||||
return AppLocalizations(locale);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldReload(LocalizationsDelegate<AppLocalizations> old) => false;
|
||||
}
|
||||
116
lib/l10n/app_pt.arb
Normal file
116
lib/l10n/app_pt.arb
Normal file
@@ -0,0 +1,116 @@
|
||||
{
|
||||
"@@locale": "pt",
|
||||
"@@last_modified": "2024-05-06T21:26:00.000Z",
|
||||
"appTitle": "Assistente de Estudo IA",
|
||||
"@appTitle": {
|
||||
"description": "Title of the application"
|
||||
},
|
||||
"schoolName": "Escola Profissional de Vila do Conde",
|
||||
"@schoolName": {
|
||||
"description": "Name of the school"
|
||||
},
|
||||
"welcomeBack": "Bem-vindo de volta",
|
||||
"@welcomeBack": {
|
||||
"description": "Welcome back message on login screen"
|
||||
},
|
||||
"signInToContinue": "Inicie sessão para continuar a aprender",
|
||||
"@signInToContinue": {
|
||||
"description": "Subtitle on login screen"
|
||||
},
|
||||
"email": "Email",
|
||||
"@email": {
|
||||
"description": "Email field label"
|
||||
},
|
||||
"password": "Palavra-passe",
|
||||
"@password": {
|
||||
"description": "Password field label"
|
||||
},
|
||||
"enterYourEmail": "Introduza o seu email",
|
||||
"@enterYourEmail": {
|
||||
"description": "Email field hint"
|
||||
},
|
||||
"enterYourPassword": "Introduza a sua palavra-passe",
|
||||
"@enterYourPassword": {
|
||||
"description": "Password field hint"
|
||||
},
|
||||
"signIn": "Iniciar Sessão",
|
||||
"@signIn": {
|
||||
"description": "Sign in button text"
|
||||
},
|
||||
"dontHaveAccount": "Não tem uma conta? ",
|
||||
"@dontHaveAccount": {
|
||||
"description": "Sign up prompt text"
|
||||
},
|
||||
"signUp": "Registar-se",
|
||||
"@signUp": {
|
||||
"description": "Sign up link text"
|
||||
},
|
||||
"createAccount": "Criar Conta",
|
||||
"@createAccount": {
|
||||
"description": "Create account title"
|
||||
},
|
||||
"joinOurCommunity": "Junte-se à nossa comunidade de aprendizagem",
|
||||
"@joinOurCommunity": {
|
||||
"description": "Sign up subtitle"
|
||||
},
|
||||
"confirmPassword": "Confirmar palavra-passe",
|
||||
"@confirmPassword": {
|
||||
"description": "Confirm password field label"
|
||||
},
|
||||
"confirmYourPassword": "Confirme a sua palavra-passe",
|
||||
"@confirmYourPassword": {
|
||||
"description": "Confirm password field hint"
|
||||
},
|
||||
"signUpButton": "Registar",
|
||||
"@signUpButton": {
|
||||
"description": "Sign up button text"
|
||||
},
|
||||
"alreadyHaveAccount": "Já tem uma conta? ",
|
||||
"@alreadyHaveAccount": {
|
||||
"description": "Login prompt text"
|
||||
},
|
||||
"login": "Iniciar Sessão",
|
||||
"@login": {
|
||||
"description": "Login link text"
|
||||
},
|
||||
"studentDashboard": "Painel do Aluno",
|
||||
"@studentDashboard": {
|
||||
"description": "Student dashboard title"
|
||||
},
|
||||
"teacherDashboard": "Painel do Professor",
|
||||
"@teacherDashboard": {
|
||||
"description": "Teacher dashboard title"
|
||||
},
|
||||
"aiTutor": "Tutor IA",
|
||||
"@aiTutor": {
|
||||
"description": "AI Tutor title"
|
||||
},
|
||||
"quizzes": "Questionários",
|
||||
"@quizzes": {
|
||||
"description": "Quizzes title"
|
||||
},
|
||||
"profile": "Perfil",
|
||||
"@profile": {
|
||||
"description": "Profile title"
|
||||
},
|
||||
"pageNotFound": "Página Não Encontrada",
|
||||
"@pageNotFound": {
|
||||
"description": "Page not found title"
|
||||
},
|
||||
"loading": "A carregar...",
|
||||
"@loading": {
|
||||
"description": "Loading message"
|
||||
},
|
||||
"error": {
|
||||
"pleaseEnterEmail": "Por favor, introduza o seu email",
|
||||
"pleaseEnterValidEmail": "Por favor, introduza um email válido",
|
||||
"pleaseEnterPassword": "Por favor, introduza a sua palavra-passe",
|
||||
"passwordTooShort": "A palavra-passe deve ter pelo menos 6 caracteres",
|
||||
"passwordsDoNotMatch": "As palavras-passe não coincidem",
|
||||
"loginFailed": "Falha no início de sessão",
|
||||
"signupFailed": "Falha no registo"
|
||||
},
|
||||
"@@error": {
|
||||
"description": "Error messages"
|
||||
}
|
||||
}
|
||||
140
lib/main.dart
140
lib/main.dart
@@ -1,122 +1,42 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'core/theme/app_theme.dart';
|
||||
import 'core/routing/app_router.dart';
|
||||
import 'core/services/firebase/firebase_service.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Initialize Firebase
|
||||
await FirebaseService.initialize();
|
||||
|
||||
runApp(const ProviderScope(child: MyApp()));
|
||||
}
|
||||
|
||||
class MyApp extends StatelessWidget {
|
||||
const MyApp({super.key});
|
||||
|
||||
// This widget is the root of your application.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
// This is the theme of your application.
|
||||
//
|
||||
// TRY THIS: Try running your application with "flutter run". You'll see
|
||||
// the application has a purple toolbar. Then, without quitting the app,
|
||||
// try changing the seedColor in the colorScheme below to Colors.green
|
||||
// and then invoke "hot reload" (save your changes or press the "hot
|
||||
// reload" button in a Flutter-supported IDE, or press "r" if you used
|
||||
// the command line to start the app).
|
||||
//
|
||||
// Notice that the counter didn't reset back to zero; the application
|
||||
// state is not lost during the reload. To reset the state, use hot
|
||||
// restart instead.
|
||||
//
|
||||
// This works for code too, not just values: Most code changes can be
|
||||
// tested with just a hot reload.
|
||||
colorScheme: .fromSeed(seedColor: Colors.deepPurple),
|
||||
),
|
||||
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MyHomePage extends StatefulWidget {
|
||||
const MyHomePage({super.key, required this.title});
|
||||
|
||||
// This widget is the home page of your application. It is stateful, meaning
|
||||
// that it has a State object (defined below) that contains fields that affect
|
||||
// how it looks.
|
||||
|
||||
// This class is the configuration for the state. It holds the values (in this
|
||||
// case the title) provided by the parent (in this case the App widget) and
|
||||
// used by the build method of the State. Fields in a Widget subclass are
|
||||
// always marked "final".
|
||||
|
||||
final String title;
|
||||
|
||||
@override
|
||||
State<MyHomePage> createState() => _MyHomePageState();
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
int _counter = 0;
|
||||
|
||||
void _incrementCounter() {
|
||||
setState(() {
|
||||
// This call to setState tells the Flutter framework that something has
|
||||
// changed in this State, which causes it to rerun the build method below
|
||||
// so that the display can reflect the updated values. If we changed
|
||||
// _counter without calling setState(), then the build method would not be
|
||||
// called again, and so nothing would appear to happen.
|
||||
_counter++;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// This method is rerun every time setState is called, for instance as done
|
||||
// by the _incrementCounter method above.
|
||||
//
|
||||
// The Flutter framework has been optimized to make rerunning build methods
|
||||
// fast, so that you can just rebuild anything that needs updating rather
|
||||
// than having to individually change instances of widgets.
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// TRY THIS: Try changing the color here to a specific color (to
|
||||
// Colors.amber, perhaps?) and trigger a hot reload to see the AppBar
|
||||
// change color while the other colors stay the same.
|
||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||
// Here we take the value from the MyHomePage object that was created by
|
||||
// the App.build method, and use it to set our appbar title.
|
||||
title: Text(widget.title),
|
||||
),
|
||||
body: Center(
|
||||
// Center is a layout widget. It takes a single child and positions it
|
||||
// in the middle of the parent.
|
||||
child: Column(
|
||||
// Column is also a layout widget. It takes a list of children and
|
||||
// arranges them vertically. By default, it sizes itself to fit its
|
||||
// children horizontally, and tries to be as tall as its parent.
|
||||
//
|
||||
// Column has various properties to control how it sizes itself and
|
||||
// how it positions its children. Here we use mainAxisAlignment to
|
||||
// center the children vertically; the main axis here is the vertical
|
||||
// axis because Columns are vertical (the cross axis would be
|
||||
// horizontal).
|
||||
//
|
||||
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
|
||||
// action in the IDE, or press "p" in the console), to see the
|
||||
// wireframe for each widget.
|
||||
mainAxisAlignment: .center,
|
||||
children: [
|
||||
const Text('You have pushed the button this many times:'),
|
||||
Text(
|
||||
'$_counter',
|
||||
style: Theme.of(context).textTheme.headlineMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _incrementCounter,
|
||||
tooltip: 'Increment',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
return MaterialApp.router(
|
||||
title: 'AI Study Assistant',
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: AppTheme.lightTheme,
|
||||
darkTheme: AppTheme.darkTheme,
|
||||
themeMode: ThemeMode.system,
|
||||
routerConfig: AppRouter.router,
|
||||
|
||||
// Internationalization configuration
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: const Locale('pt', 'PT'), // Set Portuguese (Portugal) as default
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
29
lib/shared/presentation/pages/loading_page.dart
Normal file
29
lib/shared/presentation/pages/loading_page.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
|
||||
class LoadingPage extends StatelessWidget {
|
||||
const LoadingPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryBlue),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Text(
|
||||
'Loading...',
|
||||
style: TextStyle(fontSize: 16, color: AppColors.textSecondary),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
48
lib/shared/presentation/pages/not_found_page.dart
Normal file
48
lib/shared/presentation/pages/not_found_page.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
|
||||
class NotFoundPage extends StatelessWidget {
|
||||
const NotFoundPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
title: const Text('Page Not Found'),
|
||||
backgroundColor: AppColors.surface,
|
||||
foregroundColor: AppColors.textPrimary,
|
||||
elevation: 0,
|
||||
),
|
||||
body: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
size: 64,
|
||||
color: AppColors.error,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Page Not Found',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'The page you are looking for does not exist.',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
240
lib/shared/presentation/widgets/custom_notification.dart
Normal file
240
lib/shared/presentation/widgets/custom_notification.dart
Normal file
@@ -0,0 +1,240 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_animate/flutter_animate.dart';
|
||||
|
||||
/// Custom notification widget that matches app design without underlines
|
||||
class CustomNotification extends StatefulWidget {
|
||||
final String message;
|
||||
final bool isSuccess;
|
||||
final VoidCallback? onDismiss;
|
||||
final Duration duration;
|
||||
|
||||
const CustomNotification({
|
||||
super.key,
|
||||
required this.message,
|
||||
required this.isSuccess,
|
||||
this.onDismiss,
|
||||
this.duration = const Duration(seconds: 3),
|
||||
});
|
||||
|
||||
@override
|
||||
State<CustomNotification> createState() => _CustomNotificationState();
|
||||
}
|
||||
|
||||
class _CustomNotificationState extends State<CustomNotification>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController _controller;
|
||||
late Animation<double> _slideAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
vsync: this,
|
||||
);
|
||||
|
||||
_slideAnimation = Tween<double>(
|
||||
begin: -1.0,
|
||||
end: 0.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.elasticOut,
|
||||
));
|
||||
|
||||
_fadeAnimation = Tween<double>(
|
||||
begin: 0.0,
|
||||
end: 1.0,
|
||||
).animate(CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
|
||||
_controller.forward();
|
||||
_autoDismiss();
|
||||
}
|
||||
|
||||
void _autoDismiss() {
|
||||
Future.delayed(widget.duration, () {
|
||||
if (mounted) {
|
||||
_dismiss();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _dismiss() {
|
||||
_controller.reverse().then((_) {
|
||||
if (mounted) {
|
||||
widget.onDismiss?.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
top: 50,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(0, _slideAnimation.value * -100),
|
||||
child: Opacity(
|
||||
opacity: _fadeAnimation.value,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: widget.isSuccess
|
||||
? [
|
||||
const Color(0xFF4CAF50),
|
||||
const Color(0xFF45A049),
|
||||
]
|
||||
: [
|
||||
const Color(0xFFE53E3E),
|
||||
const Color(0xFFC53030),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
widget.isSuccess ? Icons.check_circle : Icons.error,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
widget.isSuccess ? 'Sucesso!' : 'Erro',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
decoration: TextDecoration.none, // Remove underline
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
widget.message,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
decoration: TextDecoration.none, // Remove underline
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _dismiss,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: const Icon(
|
||||
Icons.close,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
).animate().scale(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.elasticOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper class to show custom notifications without underlines
|
||||
class NotificationHelper {
|
||||
static OverlayEntry? _overlayEntry;
|
||||
|
||||
static void showSuccess(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
_showNotification(
|
||||
context,
|
||||
message: message,
|
||||
isSuccess: true,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
static void showError(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
Duration duration = const Duration(seconds: 3),
|
||||
}) {
|
||||
_showNotification(
|
||||
context,
|
||||
message: message,
|
||||
isSuccess: false,
|
||||
duration: duration,
|
||||
);
|
||||
}
|
||||
|
||||
static void _showNotification(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
required bool isSuccess,
|
||||
required Duration duration,
|
||||
}) {
|
||||
// Dismiss previous notification if exists
|
||||
dismiss();
|
||||
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => CustomNotification(
|
||||
message: message,
|
||||
isSuccess: isSuccess,
|
||||
duration: duration,
|
||||
onDismiss: dismiss,
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
static void dismiss() {
|
||||
if (_overlayEntry != null) {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user