// lib/screens/contribute_screen.dart import 'dart:io'; import '../core/app_decoration.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:image_picker/image_picker.dart'; class ContributeScreen extends StatefulWidget { const ContributeScreen({Key? key}) : super(key: key); @override State createState() => _ContributeScreenState(); } class _ContributeScreenState extends State with SingleTickerProviderStateMixin { // Primary accent used for buttons / active tab (kept as a single constant) static const Color _primary = Color(0xFF0B63D6); // single corner radius to use everywhere static const double _cornerRadius = 18.0; // Form controllers final TextEditingController _titleCtl = TextEditingController(); final TextEditingController _locationCtl = TextEditingController(); final TextEditingController _organizerCtl = TextEditingController(); final TextEditingController _descriptionCtl = TextEditingController(); DateTime? _selectedDate; String _selectedCategory = 'Music'; // Image pickers final ImagePicker _picker = ImagePicker(); XFile? _coverImageFile; XFile? _thumbImageFile; bool _submitting = false; // Tab state: 0 = Contribute, 1 = Leaderboard, 2 = Achievements int _activeTab = 0; // Example progress value (0..1) double _progress = 0.45; // A few category options final List _categories = ['Music', 'Food', 'Arts', 'Sports', 'Tech', 'Community']; @override void dispose() { _titleCtl.dispose(); _locationCtl.dispose(); _organizerCtl.dispose(); _descriptionCtl.dispose(); super.dispose(); } Future _pickCoverImage() async { try { final XFile? picked = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 85); if (picked != null) setState(() => _coverImageFile = picked); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pick image: $e'))); } } Future _pickThumbnailImage() async { try { final XFile? picked = await _picker.pickImage(source: ImageSource.gallery, imageQuality: 85); if (picked != null) setState(() => _thumbImageFile = picked); } catch (e) { if (mounted) ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to pick image: $e'))); } } Future _pickDate() async { final now = DateTime.now(); final picked = await showDatePicker( context: context, initialDate: _selectedDate ?? now, firstDate: DateTime(now.year - 2), lastDate: DateTime(now.year + 3), builder: (context, child) { return Theme( data: Theme.of(context).copyWith(colorScheme: ColorScheme.light(primary: _primary)), child: child!, ); }, ); if (picked != null) setState(() => _selectedDate = picked); } Future _submit() async { if (_titleCtl.text.trim().isEmpty) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Please enter an event title'))); return; } setState(() => _submitting = true); // simulate work await Future.delayed(const Duration(milliseconds: 800)); setState(() => _submitting = false); if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Submitted for verification (demo)'))); _clearForm(); } } void _clearForm() { _titleCtl.clear(); _locationCtl.clear(); _organizerCtl.clear(); _descriptionCtl.clear(); _selectedDate = null; _selectedCategory = _categories.first; _coverImageFile = null; _thumbImageFile = null; setState(() {}); } // ---------- UI Builders ---------- Widget _buildHeader(BuildContext context) { final theme = Theme.of(context); // header uses AppDecoration.blueGradient for background (project-specific) return Container( width: double.infinity, padding: const EdgeInsets.fromLTRB(20, 32, 20, 24), decoration: AppDecoration.blueGradient.copyWith( borderRadius: BorderRadius.only( bottomLeft: Radius.circular(_cornerRadius), bottomRight: Radius.circular(_cornerRadius), ), // subtle shadow only boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.06), blurRadius: 8, offset: const Offset(0, 4)), ], ), child: SafeArea( bottom: false, child: Column( children: [ // increased spacing to create a breathable layout const SizedBox(height: 6), // Title & subtitle (centered) — smaller title weight, clearer hierarchy Text( 'Contributor Dashboard', textAlign: TextAlign.center, style: theme.textTheme.titleLarge?.copyWith( color: Colors.white, fontWeight: FontWeight.w700, fontSize: 20, ), ), const SizedBox(height: 12), // more space between title & subtitle Text( 'Track your impact, earn rewards, and climb the ranks!', textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white.withOpacity(0.92), fontSize: 13, height: 1.35), ), const SizedBox(height: 20), // more space before tabs // Pill-style segmented tabs (animated active) — slimmer / minimal _buildSegmentedTabs(context), const SizedBox(height: 18), // comfortable spacing before contributor card // Contributor level card — lighter, soft border, thinner progress Container( width: double.infinity, padding: const EdgeInsets.all(14), margin: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( color: Colors.white.withOpacity(0.08), // slightly lighter than before borderRadius: BorderRadius.circular(_cornerRadius - 2), border: Border.all(color: Colors.white.withOpacity(0.09)), // soft border ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Contributor Level', style: theme.textTheme.titleSmall?.copyWith(color: Colors.white, fontWeight: FontWeight.w700)), const SizedBox(height: 8), Text('Start earning rewards by contributing!', style: theme.textTheme.bodySmall?.copyWith(color: Colors.white70)), const SizedBox(height: 12), Row( children: [ Expanded( // animated progress using TweenAnimationBuilder child: TweenAnimationBuilder( tween: Tween(begin: 0.0, end: _progress), duration: const Duration(milliseconds: 700), builder: (context, value, _) => ClipRRect( borderRadius: BorderRadius.circular(6), child: LinearProgressIndicator( value: value, minHeight: 6, // thinner progress bar valueColor: const AlwaysStoppedAnimation(Colors.white), backgroundColor: Colors.white24, ), ), ), ), const SizedBox(width: 12), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.white.withOpacity(0.12), borderRadius: BorderRadius.circular(12), ), child: Text('Explorer', style: theme.textTheme.bodySmall?.copyWith(color: Colors.white, fontWeight: FontWeight.w600)), ), ], ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text('30 pts', style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white, fontWeight: FontWeight.w600)), Text('Next: Enthusiast (50 pts)', style: theme.textTheme.bodyMedium?.copyWith(color: Colors.white70)), ], ), ], ), ), ], ), ), ); } Widget _buildSegmentedTabs(BuildContext context) { final tabs = ['Contribute', 'Leaderboard', 'Achievements']; return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white.withOpacity(0.06), borderRadius: BorderRadius.circular(_cornerRadius + 6), border: Border.all(color: Colors.white.withOpacity(0.10)), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.04), blurRadius: 8, offset: const Offset(0, 6))], ), child: Row( children: List.generate(tabs.length, (i) { final active = i == _activeTab; return Expanded( child: GestureDetector( onTap: () => setState(() => _activeTab = i), child: AnimatedContainer( duration: const Duration(milliseconds: 220), padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 10), margin: EdgeInsets.only(right: i == tabs.length - 1 ? 0 : 8), decoration: BoxDecoration( color: active ? Colors.white : Colors.transparent, borderRadius: BorderRadius.circular(_cornerRadius - 4), boxShadow: active ? [BoxShadow(color: Colors.black.withOpacity(0.06), blurRadius: 8, offset: const Offset(0, 4))] : null, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // show a small icon only for active tab if (active) ...[ Icon(Icons.edit, size: 14, color: _primary), const SizedBox(width: 8), ], // FittedBox ensures the whole word is visible (scales down if necessary) Flexible( child: FittedBox( fit: BoxFit.scaleDown, alignment: Alignment.center, child: Text( tabs[i], textAlign: TextAlign.center, style: TextStyle( color: active ? _primary : Colors.white.withOpacity(0.95), fontWeight: active ? FontWeight.w800 : FontWeight.w600, fontSize: 16, // preferred size; FittedBox will shrink if needed ), ), ), ), ], ), ), ), ); }), ), ), ); } Widget _buildForm(BuildContext ctx) { final theme = Theme.of(ctx); return Container( width: double.infinity, margin: const EdgeInsets.only(top: 18), padding: const EdgeInsets.fromLTRB(18, 20, 18, 28), decoration: BoxDecoration( color: theme.scaffoldBackgroundColor, borderRadius: BorderRadius.circular(_cornerRadius), boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.03), blurRadius: 12, offset: const Offset(0, 6))], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // sheet title Center( child: Column( children: [ Text('Contribute an Event', style: theme.textTheme.headlineSmall?.copyWith(fontWeight: FontWeight.bold)), const SizedBox(height: 8), Text('Share local events. Earn points for every verified submission!', textAlign: TextAlign.center, style: theme.textTheme.bodyMedium?.copyWith(color: theme.hintColor)), ], ), ), const SizedBox(height: 18), // small helper button Center( child: OutlinedButton( onPressed: () { ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Want to edit an existing event? (demo)'))); }, style: OutlinedButton.styleFrom( side: BorderSide(color: Colors.grey.shade300), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(_cornerRadius - 6)), padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 12), ), child: const Text('Want to edit an existing event?'), ), ), const SizedBox(height: 18), // Event Title Text('Event Title', style: theme.textTheme.labelLarge), const SizedBox(height: 8), _roundedTextField(controller: _titleCtl, hint: 'e.g. Local Food Festival'), const SizedBox(height: 14), // Category Text('Category', style: theme.textTheme.labelLarge), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)), child: DropdownButtonHideUnderline( child: DropdownButton( value: _selectedCategory, isExpanded: true, items: _categories.map((c) => DropdownMenuItem(value: c, child: Text(c))).toList(), onChanged: (v) => setState(() => _selectedCategory = v ?? _selectedCategory), icon: const Icon(Icons.keyboard_arrow_down), ), ), ), const SizedBox(height: 14), // Date Text('Date', style: theme.textTheme.labelLarge), const SizedBox(height: 8), GestureDetector( onTap: _pickDate, child: Container( height: 48, padding: const EdgeInsets.symmetric(horizontal: 12), alignment: Alignment.centerLeft, decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)), child: Text(_selectedDate == null ? 'Select date' : '${_selectedDate!.day}/${_selectedDate!.month}/${_selectedDate!.year}', style: theme.textTheme.bodyMedium), ), ), const SizedBox(height: 14), // Location Text('Location', style: theme.textTheme.labelLarge), const SizedBox(height: 8), _roundedTextField(controller: _locationCtl, hint: 'e.g. City Park, Calicut'), const SizedBox(height: 14), // Organizer Text('Organizer Name', style: theme.textTheme.labelLarge), const SizedBox(height: 8), _roundedTextField(controller: _organizerCtl, hint: 'Individual or Organization Name'), const SizedBox(height: 14), // Description Text('Description', style: theme.textTheme.labelLarge), const SizedBox(height: 8), TextField( controller: _descriptionCtl, minLines: 4, maxLines: 6, decoration: InputDecoration( hintText: 'Tell us more about the event...', filled: true, fillColor: theme.cardColor, border: OutlineInputBorder(borderRadius: BorderRadius.circular(_cornerRadius - 8), borderSide: BorderSide.none), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), ), ), const SizedBox(height: 18), // Event Images header Text('Event Images', style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600)), const SizedBox(height: 12), // Cover image Text('Cover Image', style: theme.textTheme.bodySmall), const SizedBox(height: 8), GestureDetector( onTap: _pickCoverImage, child: _imagePickerPlaceholder(file: _coverImageFile, label: 'Cover Image'), ), const SizedBox(height: 12), // Thumbnail image Text('Thumbnail', style: theme.textTheme.bodySmall), const SizedBox(height: 8), GestureDetector( onTap: _pickThumbnailImage, child: _imagePickerPlaceholder(file: _thumbImageFile, label: 'Thumbnail'), ), const SizedBox(height: 22), // Submit button SizedBox( width: double.infinity, height: 52, child: ElevatedButton( onPressed: _submitting ? null : _submit, style: ElevatedButton.styleFrom( backgroundColor: _primary, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(_cornerRadius - 6)), ), child: _submitting ? const SizedBox(width: 22, height: 22, child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2.2)) : const Text('Submit for Verification', style: TextStyle(fontWeight: FontWeight.w600)), ), ), ], ), ); } Widget _roundedTextField({required TextEditingController controller, required String hint}) { final theme = Theme.of(context); return TextField( controller: controller, decoration: InputDecoration( hintText: hint, filled: true, fillColor: theme.cardColor, border: OutlineInputBorder(borderRadius: BorderRadius.circular(_cornerRadius - 8), borderSide: BorderSide.none), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), ), ); } Widget _imagePickerPlaceholder({XFile? file, required String label}) { final theme = Theme.of(context); if (file == null) { return Container( width: double.infinity, height: 120, decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)), child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.image, size: 28, color: theme.hintColor), const SizedBox(height: 8), Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor)), ], ), ), ); } // show picked image (file or network depending on platform) if (kIsWeb || file.path.startsWith('http')) { return ClipRRect( borderRadius: BorderRadius.circular(_cornerRadius - 8), child: Image.network(file.path, width: double.infinity, height: 120, fit: BoxFit.cover, errorBuilder: (_, __, ___) { return Container( width: double.infinity, height: 120, decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)), child: Center(child: Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor))), ); }), ); } else { final f = File(file.path); if (!f.existsSync()) { return Container( width: double.infinity, height: 120, decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)), child: Center(child: Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor))), ); } return ClipRRect( borderRadius: BorderRadius.circular(_cornerRadius - 8), child: Image.file(f, width: double.infinity, height: 120, fit: BoxFit.cover, errorBuilder: (_, __, ___) { return Container( width: double.infinity, height: 120, decoration: BoxDecoration(color: theme.cardColor, borderRadius: BorderRadius.circular(_cornerRadius - 8)), child: Center(child: Text(label, style: theme.textTheme.bodySmall?.copyWith(color: theme.hintColor))), ); }), ); } } @override Widget build(BuildContext context) { // The whole screen is scrollable — header is part of the normal scroll (not floating). return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, body: SafeArea( bottom: false, child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( children: [ _buildHeader(context), Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: _buildForm(context), ), const SizedBox(height: 36), ], ), ), ), ); } }