Initial commit: SIBU 2.0 MISSION

This commit is contained in:
2026-02-21 09:53:31 -05:00
commit 0c7aa53c8b
400 changed files with 67708 additions and 0 deletions

View File

@ -0,0 +1,207 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../../../core/app_export.dart';
class BusRouteCardWidget extends StatefulWidget {
final Map<String, dynamic> routeData;
final VoidCallback? onNotificationToggle;
const BusRouteCardWidget({
super.key,
required this.routeData,
this.onNotificationToggle,
});
@override
State<BusRouteCardWidget> createState() => _BusRouteCardWidgetState();
}
class _BusRouteCardWidgetState extends State<BusRouteCardWidget> {
bool _isNotificationEnabled = false;
void _toggleNotification() {
setState(() {
_isNotificationEnabled = !_isNotificationEnabled;
});
if (widget.onNotificationToggle != null) {
widget.onNotificationToggle!();
}
}
String _formatTime(int minutes) {
if (minutes < 60) {
return '${minutes}min';
} else {
final hours = minutes ~/ 60;
final remainingMinutes = minutes % 60;
return remainingMinutes > 0
? '${hours}h ${remainingMinutes}min'
: '${hours}h';
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final routeName = widget.routeData['routeName'] as String? ?? '';
final nextBusMinutes = widget.routeData['nextBusMinutes'] as int? ?? 0;
final upcomingTimes =
(widget.routeData['upcomingTimes'] as List?)?.cast<String>() ?? [];
final isDelayed = widget.routeData['isDelayed'] as bool? ?? false;
return Container(
margin: EdgeInsets.symmetric(horizontal: 4.w, vertical: 1.h),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Padding(
padding: EdgeInsets.all(4.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Route header with notification toggle
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
routeName,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
if (isDelayed) ...[
SizedBox(height: 0.5.h),
Container(
padding: EdgeInsets.symmetric(
horizontal: 2.w, vertical: 0.5.h),
decoration: BoxDecoration(
color:
AppTheme.warningOrange.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Retraso reportado',
style: theme.textTheme.labelSmall?.copyWith(
color: AppTheme.warningOrange,
fontWeight: FontWeight.w500,
),
),
),
],
],
),
),
GestureDetector(
onTap: _toggleNotification,
child: Container(
padding: EdgeInsets.all(2.w),
decoration: BoxDecoration(
color: _isNotificationEnabled
? AppTheme.accentYellow.withValues(alpha: 0.1)
: Colors.transparent,
borderRadius: BorderRadius.circular(8),
),
child: CustomIconWidget(
iconName: _isNotificationEnabled
? 'notifications'
: 'notifications_none',
color: _isNotificationEnabled
? AppTheme.accentYellow
: theme.colorScheme.onSurface.withValues(alpha: 0.6),
size: 24,
),
),
),
],
),
SizedBox(height: 3.h),
// Next bus countdown
Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: AppTheme.accentYellow.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.accentYellow.withValues(alpha: 0.3),
width: 1,
),
),
child: Column(
children: [
Text(
'Próximo bus en:',
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
SizedBox(height: 1.h),
Text(
_formatTime(nextBusMinutes),
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
color: AppTheme.primaryBlack,
),
),
],
),
),
// Upcoming times
if (upcomingTimes.isNotEmpty) ...[
SizedBox(height: 3.h),
Text(
'Próximas salidas:',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface,
),
),
SizedBox(height: 1.h),
Wrap(
spacing: 2.w,
runSpacing: 1.h,
children: upcomingTimes
.take(4)
.map((time) => Container(
padding: EdgeInsets.symmetric(
horizontal: 3.w, vertical: 1.h),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: theme.colorScheme.outline
.withValues(alpha: 0.3),
width: 1,
),
),
child: Text(
time,
style: theme.textTheme.bodySmall?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface,
),
),
))
.toList(),
),
],
],
),
),
);
}
}

View File

@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../../../core/app_export.dart';
class NearbyLandmarksWidget extends StatelessWidget {
final List<Map<String, dynamic>> landmarks;
const NearbyLandmarksWidget({
super.key,
required this.landmarks,
});
Widget _buildLandmarkItem(
BuildContext context, Map<String, dynamic> landmark) {
final theme = Theme.of(context);
final name = landmark['name'] as String? ?? '';
final distance = landmark['distance'] as String? ?? '';
final type = landmark['type'] as String? ?? '';
String iconName = 'place';
switch (type.toLowerCase()) {
case 'restaurant':
iconName = 'restaurant';
break;
case 'hospital':
iconName = 'local_hospital';
break;
case 'school':
iconName = 'school';
break;
case 'bank':
iconName = 'account_balance';
break;
case 'store':
iconName = 'store';
break;
case 'gas_station':
iconName = 'local_gas_station';
break;
default:
iconName = 'place';
}
return Container(
margin: EdgeInsets.only(bottom: 2.h),
padding: EdgeInsets.all(3.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.outline.withValues(alpha: 0.2),
width: 1,
),
),
child: Row(
children: [
Container(
width: 10.w,
height: 5.h,
decoration: BoxDecoration(
color: AppTheme.primaryBlack.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(6),
),
child: Center(
child: CustomIconWidget(
iconName: iconName,
color: AppTheme.primaryBlack,
size: 20,
),
),
),
SizedBox(width: 3.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: theme.colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 0.5.h),
Text(
distance,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
if (landmarks.isEmpty) {
return Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
CustomIconWidget(
iconName: 'location_off',
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
size: 32,
),
SizedBox(height: 2.h),
Text(
'No hay puntos de referencia cercanos',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
textAlign: TextAlign.center,
),
],
),
);
}
return Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Puntos de referencia cercanos',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
SizedBox(height: 3.h),
...landmarks
.map((landmark) => _buildLandmarkItem(context, landmark))
.toList(),
],
),
);
}
}

View File

@ -0,0 +1,396 @@
import 'package:camera/camera.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:sizer/sizer.dart';
import '../../../../core/app_export.dart';
class ReportIssueWidget extends StatefulWidget {
final VoidCallback? onReportSubmitted;
const ReportIssueWidget({
super.key,
this.onReportSubmitted,
});
@override
State<ReportIssueWidget> createState() => _ReportIssueWidgetState();
}
class _ReportIssueWidgetState extends State<ReportIssueWidget> {
final TextEditingController _issueController = TextEditingController();
String _selectedIssueType = 'Limpieza';
XFile? _capturedImage;
CameraController? _cameraController;
List<CameraDescription> _cameras = [];
bool _isCameraInitialized = false;
bool _isSubmitting = false;
final List<String> _issueTypes = [
'Limpieza',
'Daños en la estructura',
'Falta de iluminación',
'Problemas de accesibilidad',
'Vandalismo',
'Otro',
];
@override
void initState() {
super.initState();
_initializeCamera();
}
@override
void dispose() {
_issueController.dispose();
_cameraController?.dispose();
super.dispose();
}
Future<bool> _requestCameraPermission() async {
if (kIsWeb) return true;
return (await Permission.camera.request()).isGranted;
}
Future<void> _initializeCamera() async {
try {
if (!await _requestCameraPermission()) return;
_cameras = await availableCameras();
if (_cameras.isEmpty) return;
final camera = kIsWeb
? _cameras.firstWhere(
(c) => c.lensDirection == CameraLensDirection.front,
orElse: () => _cameras.first)
: _cameras.firstWhere(
(c) => c.lensDirection == CameraLensDirection.back,
orElse: () => _cameras.first);
_cameraController = CameraController(
camera, kIsWeb ? ResolutionPreset.medium : ResolutionPreset.high);
await _cameraController!.initialize();
if (!kIsWeb) {
try {
await _cameraController!.setFocusMode(FocusMode.auto);
await _cameraController!.setFlashMode(FlashMode.auto);
} catch (e) {}
}
if (mounted) {
setState(() {
_isCameraInitialized = true;
});
}
} catch (e) {
// Silent fail - camera not available
}
}
Future<void> _capturePhoto() async {
if (_cameraController == null || !_cameraController!.value.isInitialized)
return;
try {
final XFile photo = await _cameraController!.takePicture();
setState(() {
_capturedImage = photo;
});
} catch (e) {
// Silent fail
}
}
Future<void> _pickImageFromGallery() async {
try {
final ImagePicker picker = ImagePicker();
final XFile? image = await picker.pickImage(source: ImageSource.gallery);
if (image != null) {
setState(() {
_capturedImage = image;
});
}
} catch (e) {
// Silent fail
}
}
Future<void> _submitReport() async {
if (_issueController.text.trim().isEmpty) return;
setState(() {
_isSubmitting = true;
});
// Simulate report submission
await Future.delayed(const Duration(seconds: 2));
if (mounted) {
setState(() {
_isSubmitting = false;
_issueController.clear();
_capturedImage = null;
_selectedIssueType = 'Limpieza';
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Reporte enviado exitosamente'),
backgroundColor: AppTheme.successGreen,
),
);
if (widget.onReportSubmitted != null) {
widget.onReportSubmitted!();
}
}
}
void _showReportDialog() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: 85.h,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
),
child: Padding(
padding: EdgeInsets.all(4.w),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Handle bar
Center(
child: Container(
width: 12.w,
height: 0.5.h,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(2),
),
),
),
SizedBox(height: 3.h),
// Header
Row(
children: [
Expanded(
child: Text(
'Reportar problema',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
),
GestureDetector(
onTap: () => Navigator.pop(context),
child: CustomIconWidget(
iconName: 'close',
color: Theme.of(context).colorScheme.onSurface,
size: 24,
),
),
],
),
SizedBox(height: 4.h),
// Issue type dropdown
Text(
'Tipo de problema',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 1.h),
Container(
width: double.infinity,
padding: EdgeInsets.symmetric(horizontal: 4.w),
decoration: BoxDecoration(
border:
Border.all(color: Theme.of(context).colorScheme.outline),
borderRadius: BorderRadius.circular(8),
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: _selectedIssueType,
isExpanded: true,
items: _issueTypes
.map((type) => DropdownMenuItem(
value: type,
child: Text(type),
))
.toList(),
onChanged: (value) {
if (value != null) {
setState(() {
_selectedIssueType = value;
});
}
},
),
),
),
SizedBox(height: 3.h),
// Description field
Text(
'Descripción del problema',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 1.h),
TextField(
controller: _issueController,
maxLines: 4,
decoration: const InputDecoration(
hintText:
'Describe el problema que encontraste en esta parada...',
),
),
SizedBox(height: 3.h),
// Photo section
Text(
'Agregar foto (opcional)',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 1.h),
if (_capturedImage != null) ...[
Container(
width: double.infinity,
height: 20.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: Theme.of(context).colorScheme.outline),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: kIsWeb
? Image.network(_capturedImage!.path, fit: BoxFit.cover)
: Image.network(_capturedImage!.path,
fit: BoxFit.cover),
),
),
SizedBox(height: 2.h),
],
Row(
children: [
if (_isCameraInitialized) ...[
Expanded(
child: ElevatedButton.icon(
onPressed: _capturePhoto,
icon: CustomIconWidget(
iconName: 'camera_alt',
color: AppTheme.primaryBlack,
size: 20,
),
label: const Text('Tomar foto'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.accentYellow,
foregroundColor: AppTheme.primaryBlack,
),
),
),
SizedBox(width: 2.w),
],
Expanded(
child: OutlinedButton.icon(
onPressed: _pickImageFromGallery,
icon: CustomIconWidget(
iconName: 'photo_library',
color: Theme.of(context).colorScheme.primary,
size: 20,
),
label: const Text('Galería'),
),
),
],
),
const Spacer(),
// Submit button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _isSubmitting ? null : _submitReport,
child: _isSubmitting
? const CircularProgressIndicator()
: const Text('Enviar reporte'),
),
),
SizedBox(height: 2.h),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Reportar problema',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
SizedBox(height: 2.h),
Text(
'¿Encontraste algún problema en esta parada? Ayúdanos a mejorar reportándolo.',
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.7),
),
),
SizedBox(height: 3.h),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: _showReportDialog,
icon: CustomIconWidget(
iconName: 'report_problem',
color: AppTheme.primaryBlack,
size: 20,
),
label: const Text('Reportar problema'),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.warningOrange,
foregroundColor: AppTheme.primaryBlack,
padding: EdgeInsets.symmetric(vertical: 3.h),
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../../../core/app_export.dart';
class StopAmenitiesWidget extends StatelessWidget {
final Map<String, dynamic> amenitiesData;
const StopAmenitiesWidget({
super.key,
required this.amenitiesData,
});
Widget _buildAmenityItem(
BuildContext context, String iconName, String label, bool isAvailable) {
final theme = Theme.of(context);
return Column(
children: [
Container(
width: 12.w,
height: 6.h,
decoration: BoxDecoration(
color: isAvailable
? AppTheme.successGreen.withValues(alpha: 0.1)
: theme.colorScheme.surface.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isAvailable
? AppTheme.successGreen.withValues(alpha: 0.3)
: theme.colorScheme.outline.withValues(alpha: 0.3),
width: 1,
),
),
child: Center(
child: CustomIconWidget(
iconName: iconName,
color: isAvailable
? AppTheme.successGreen
: theme.colorScheme.onSurface.withValues(alpha: 0.4),
size: 24,
),
),
),
SizedBox(height: 1.h),
Text(
label,
style: theme.textTheme.labelSmall?.copyWith(
color: isAvailable
? theme.colorScheme.onSurface
: theme.colorScheme.onSurface.withValues(alpha: 0.5),
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
),
],
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final hasShelter = amenitiesData['hasShelter'] as bool? ?? false;
final hasBench = amenitiesData['hasBench'] as bool? ?? false;
final isAccessible = amenitiesData['isAccessible'] as bool? ?? false;
final hasLighting = amenitiesData['hasLighting'] as bool? ?? false;
final hasTrashCan = amenitiesData['hasTrashCan'] as bool? ?? false;
return Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Comodidades de la parada',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
SizedBox(height: 3.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: _buildAmenityItem(
context,
'home',
'Refugio',
hasShelter,
),
),
Expanded(
child: _buildAmenityItem(
context,
'chair',
'Asiento',
hasBench,
),
),
Expanded(
child: _buildAmenityItem(
context,
'accessible',
'Accesible',
isAccessible,
),
),
Expanded(
child: _buildAmenityItem(
context,
'lightbulb',
'Iluminación',
hasLighting,
),
),
Expanded(
child: _buildAmenityItem(
context,
'delete',
'Basura',
hasTrashCan,
),
),
],
),
],
),
);
}
}

View File

@ -0,0 +1,236 @@
import 'package:flutter/material.dart';
import 'package:sizer/sizer.dart';
import '../../../../core/app_export.dart';
class UserCommentsWidget extends StatefulWidget {
final List<Map<String, dynamic>> comments;
final VoidCallback? onAddComment;
const UserCommentsWidget({
super.key,
required this.comments,
this.onAddComment,
});
@override
State<UserCommentsWidget> createState() => _UserCommentsWidgetState();
}
class _UserCommentsWidgetState extends State<UserCommentsWidget> {
bool _isExpanded = false;
Widget _buildCommentItem(BuildContext context, Map<String, dynamic> comment) {
final theme = Theme.of(context);
final userName = comment['userName'] as String? ?? '';
final userAvatar = comment['userAvatar'] as String? ?? '';
final semanticLabel = comment['semanticLabel'] as String? ?? '';
final commentText = comment['comment'] as String? ?? '';
final timestamp = comment['timestamp'] as String? ?? '';
final rating = comment['rating'] as int? ?? 0;
return Container(
margin: EdgeInsets.only(bottom: 3.h),
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: theme.colorScheme.outline.withValues(alpha: 0.2),
width: 1,
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// User info and rating
Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: CustomImageWidget(
imageUrl: userAvatar,
width: 40,
height: 40,
fit: BoxFit.cover,
semanticLabel: semanticLabel,
),
),
SizedBox(width: 3.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
userName,
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
Text(
timestamp,
style: theme.textTheme.bodySmall?.copyWith(
color:
theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
),
],
),
),
// Rating stars
Row(
children: List.generate(
5,
(index) => CustomIconWidget(
iconName: index < rating ? 'star' : 'star_border',
color: index < rating
? AppTheme.accentYellow
: theme.colorScheme.onSurface
.withValues(alpha: 0.3),
size: 16,
)),
),
],
),
SizedBox(height: 2.h),
// Comment text
Text(
commentText,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onSurface,
),
),
],
),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
width: double.infinity,
padding: EdgeInsets.all(4.w),
decoration: BoxDecoration(
color: theme.colorScheme.surface,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: theme.shadowColor.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header with expand/collapse
GestureDetector(
onTap: () {
setState(() {
_isExpanded = !_isExpanded;
});
},
child: Row(
children: [
Expanded(
child: Text(
'Comentarios de usuarios (${widget.comments.length})',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: theme.colorScheme.onSurface,
),
),
),
CustomIconWidget(
iconName: _isExpanded ? 'expand_less' : 'expand_more',
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
size: 24,
),
],
),
),
if (_isExpanded) ...[
SizedBox(height: 3.h),
// Add comment button
GestureDetector(
onTap: widget.onAddComment,
child: Container(
width: double.infinity,
padding: EdgeInsets.all(3.w),
decoration: BoxDecoration(
color: AppTheme.accentYellow.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: AppTheme.accentYellow.withValues(alpha: 0.3),
width: 1,
),
),
child: Row(
children: [
CustomIconWidget(
iconName: 'add_comment',
color: AppTheme.primaryBlack,
size: 20,
),
SizedBox(width: 2.w),
Text(
'Agregar comentario',
style: theme.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w500,
color: AppTheme.primaryBlack,
),
),
],
),
),
),
SizedBox(height: 3.h),
// Comments list
if (widget.comments.isEmpty)
Container(
width: double.infinity,
padding: EdgeInsets.all(6.w),
child: Column(
children: [
CustomIconWidget(
iconName: 'comment',
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
size: 32,
),
SizedBox(height: 2.h),
Text(
'Aún no hay comentarios',
style: theme.textTheme.bodyMedium?.copyWith(
color:
theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
textAlign: TextAlign.center,
),
SizedBox(height: 1.h),
Text(
'¡Sé el primero en compartir tu experiencia!',
style: theme.textTheme.bodySmall?.copyWith(
color:
theme.colorScheme.onSurface.withValues(alpha: 0.5),
),
textAlign: TextAlign.center,
),
],
),
)
else
...widget.comments
.map((comment) => _buildCommentItem(context, comment))
.toList(),
],
],
),
);
}
}