feat: Phase 3 — 26 medium-priority gaps implemented
P3-A/K Profile: Eventify ID glassmorphic badge (tap-to-copy), DiceBear
Notionists avatar via TierAvatarRing, district picker (14 pills)
with 183-day cooldown lock, multipart photo upload to server
P3-B Home: Top Events converted to PageView scroll-snap
(viewportFraction 0.9 + PageScrollPhysics)
P3-C Event detail: contributor widget (tier ring + name + navigation),
related events horizontal row; added getEventsByCategory() to
EventsService; added contributorId/Name/Tier fields to EventModel
P3-D Kerala pincodes: 463-entry JSON (all 14 districts), registered as
asset, async-loaded in SearchScreen replacing hardcoded 32 cities
P3-E Checkout: promo code field + Apply/Remove button in Step 2,
discountAmount subtracted from total, applyPromo()/resetPromo()
methods in CheckoutProvider
P3-F/G Gamification: reward cycle countdown + EP→RP progress bar (blue→
amber) in contribute + profile screens; TierAvatarRing in podium
and all leaderboard rows; GlassCard current-user stats card at
top of leaderboard tab
P3-H New ContributorProfileScreen: tier ring, stats, submission grid
with status chips; getDashboardForUser() in GamificationService;
wired from leaderboard row taps
P3-I Achievements: 11 default badges (up from 6), 6 new icon map
entries; progress % labels already confirmed present
P3-J Reviews: CustomPainter circular arc rating ring (amber, 84px)
replaces large rating number in ReviewSummary
P3-L Share rank card: RepaintBoundary → PNG capture → Share.shareXFiles;
share button wired in profile header and leaderboard tab
P3-M SafeArea audit: home bottom nav, contribute/achievements scroll
padding, profile CustomScrollView top inset
New files: tier_avatar_ring.dart, glass_card.dart,
eventify_bottom_sheet.dart, contributor_profile_screen.dart,
share_rank_card.dart, assets/data/kerala_pincodes.json
New dep: path_provider ^2.1.0
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,8 @@ import '../core/auth/auth_guard.dart';
|
||||
import '../core/utils/error_utils.dart';
|
||||
import '../core/constants.dart';
|
||||
import '../features/reviews/widgets/review_section.dart';
|
||||
import '../widgets/tier_avatar_ring.dart';
|
||||
import 'contributor_profile_screen.dart';
|
||||
import 'checkout_screen.dart';
|
||||
|
||||
class LearnMoreScreen extends StatefulWidget {
|
||||
@@ -59,6 +61,10 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
// Google Map
|
||||
GoogleMapController? _mapController;
|
||||
|
||||
// Related events (EVT-002)
|
||||
List<EventModel> _relatedEvents = [];
|
||||
bool _loadingRelated = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -100,6 +106,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
_event = ev;
|
||||
});
|
||||
_startAutoScroll();
|
||||
_loadRelatedEvents();
|
||||
return; // success
|
||||
} catch (e) {
|
||||
debugPrint('_loadFullDetails attempt ${attempt + 1} failed for event ${widget.eventId}: $e');
|
||||
@@ -120,6 +127,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
if (!mounted) return;
|
||||
setState(() => _event = ev);
|
||||
_startAutoScroll();
|
||||
_loadRelatedEvents();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() => _error = userFriendlyError(e));
|
||||
@@ -128,6 +136,19 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch related events by the same event type (EVT-002).
|
||||
Future<void> _loadRelatedEvents() async {
|
||||
if (_event?.eventTypeId == null) return;
|
||||
if (mounted) setState(() => _loadingRelated = true);
|
||||
try {
|
||||
final events = await _service.getEventsByCategory(_event!.eventTypeId!, limit: 6);
|
||||
final filtered = events.where((e) => e.id != widget.eventId).take(5).toList();
|
||||
if (mounted) setState(() => _relatedEvents = filtered);
|
||||
} finally {
|
||||
if (mounted) setState(() => _loadingRelated = false);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Carousel helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -441,8 +462,12 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
if (_event!.importantInfo.isEmpty &&
|
||||
(_event!.importantInformation ?? '').isNotEmpty)
|
||||
_buildImportantInfoFallback(theme),
|
||||
// EVT-001: Contributor widget
|
||||
_buildContributorSection(theme),
|
||||
const SizedBox(height: 24),
|
||||
ReviewSection(eventId: widget.eventId),
|
||||
// EVT-002: Related events horizontal row
|
||||
_buildRelatedEventsSection(theme),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -619,11 +644,15 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
if (_event!.importantInfo.isEmpty &&
|
||||
(_event!.importantInformation ?? '').isNotEmpty)
|
||||
_buildImportantInfoFallback(theme),
|
||||
// EVT-001: Contributor widget
|
||||
_buildContributorSection(theme),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 0),
|
||||
child: ReviewSection(eventId: widget.eventId),
|
||||
),
|
||||
// EVT-002: Related events horizontal row
|
||||
_buildRelatedEventsSection(theme),
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
@@ -1335,6 +1364,227 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
||||
return items;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 8. CONTRIBUTOR WIDGET (EVT-001)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Widget _buildContributorSection(ThemeData theme) {
|
||||
final name = _event?.contributorName;
|
||||
if (name == null || name.isEmpty) return const SizedBox.shrink();
|
||||
final tier = _event!.contributorTier ?? '';
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 0),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.brightness == Brightness.dark
|
||||
? const Color(0xFF1E293B)
|
||||
: theme.cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: theme.brightness == Brightness.dark
|
||||
? Colors.white.withOpacity(0.08)
|
||||
: theme.dividerColor,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
TierAvatarRing(
|
||||
username: name,
|
||||
tier: tier,
|
||||
size: 40,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Contributed by',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.hintColor,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
name,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (tier.isNotEmpty)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 2),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.primary.withOpacity(0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
tier,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: theme.colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_event?.contributorId != null)
|
||||
IconButton(
|
||||
icon: Icon(Icons.arrow_forward_ios,
|
||||
size: 14, color: theme.hintColor),
|
||||
onPressed: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => ContributorProfileScreen(
|
||||
contributorId: _event!.contributorId!,
|
||||
contributorName: _event!.contributorName!,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 9. RELATED EVENTS ROW (EVT-002)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Widget _buildRelatedEventsSection(ThemeData theme) {
|
||||
if (_loadingRelated) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 28, 20, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Related Events',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Center(
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
width: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_relatedEvents.isEmpty) return const SizedBox.shrink();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 28, 20, 8),
|
||||
child: Text(
|
||||
'Related Events',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
itemCount: _relatedEvents.length,
|
||||
itemBuilder: (context, i) {
|
||||
final e = _relatedEvents[i];
|
||||
final displayName = e.title ?? e.name;
|
||||
final imageUrl = e.thumbImg ?? '';
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LearnMoreScreen(eventId: e.id),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
width: 140,
|
||||
margin: const EdgeInsets.only(right: 10),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
color: theme.brightness == Brightness.dark
|
||||
? const Color(0xFF1E293B)
|
||||
: theme.cardColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.shadowColor.withOpacity(0.06),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
),
|
||||
child: imageUrl.isNotEmpty
|
||||
? CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
height: 100,
|
||||
width: 140,
|
||||
fit: BoxFit.cover,
|
||||
errorWidget: (_, __, ___) => Container(
|
||||
height: 100,
|
||||
width: 140,
|
||||
color: theme.dividerColor,
|
||||
child: Icon(Icons.event,
|
||||
size: 32, color: theme.hintColor),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
height: 100,
|
||||
width: 140,
|
||||
color: theme.dividerColor,
|
||||
child: Icon(Icons.event,
|
||||
size: 32, color: theme.hintColor),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
displayName,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.35,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImportantInfoFallback(ThemeData theme) {
|
||||
final parsed = _parseHtmlImportantInfo(_event!.importantInformation!);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user