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:
2026-04-04 17:17:36 +05:30
parent fe8af7cfe6
commit 632754415d
19 changed files with 2346 additions and 183 deletions

View File

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