Telas de login e dashboard de estudante feito

This commit is contained in:
2026-05-07 21:10:30 +01:00
parent 547d5f5484
commit c1d1a0fce1
44 changed files with 6740 additions and 183 deletions

View 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%
}

View 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);
}
}

View 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';
}
}
}

View 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;
}
}
}

View 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
}

View 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,
),
),
);
}
}

View 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),
],
),
),
),
),
),
),
);
}
}

View 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');
}
}
}

View 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),
],
),
),
),
),
),
),
);
}
}

View File

@@ -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),
],
),
),
),
),
),
);
}
}

View File

@@ -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,
),
),
),
);
}
}

View File

@@ -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,
),
),
],
),
);
}
}

View File

@@ -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,
),
],
),
);
}
}

View File

@@ -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));
}
}

View 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,
),
),
),
);
}
}

View 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,
),
),
),
);
}
}

View 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,
),
),
);
}
}

View 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)),
);
}
}

View 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
View 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"
}
}

View 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
View 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"
}
}

View File

@@ -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
);
}
}

View 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),
),
],
),
),
);
}
}

View 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,
),
),
],
),
),
);
}
}

View 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;
}
}
}