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
This commit is contained in:
2026-04-04 17:17:36 +05:30
parent e365361451
commit e9752c3d61
19 changed files with 2346 additions and 183 deletions

View File

@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
void showEventifyBottomSheet(
BuildContext context, {
required String title,
required Widget child,
double initialSize = 0.5,
double minSize = 0.3,
double maxSize = 0.9,
bool isDismissible = true,
}) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
isDismissible: isDismissible,
backgroundColor: Colors.transparent,
builder: (_) => DraggableScrollableSheet(
initialChildSize: initialSize,
minChildSize: minSize,
maxChildSize: maxSize,
expand: false,
builder: (_, scrollController) => _EventifyBottomSheetContent(
title: title,
child: child,
scrollController: scrollController,
),
),
);
}
class _EventifyBottomSheetContent extends StatelessWidget {
const _EventifyBottomSheetContent({
required this.title,
required this.child,
required this.scrollController,
});
final String title;
final Widget child;
final ScrollController scrollController;
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
color: Color(0xFF0F172A),
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 12),
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.white24,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
children: [
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.white54),
onPressed: () => Navigator.pop(context),
),
],
),
),
const Divider(color: Colors.white12, height: 1),
Expanded(
child: SingleChildScrollView(
controller: scrollController,
child: child,
),
),
],
),
);
}
}

View File

@@ -0,0 +1,53 @@
import 'dart:ui';
import 'package:flutter/material.dart';
class GlassCard extends StatelessWidget {
const GlassCard({
super.key,
required this.child,
this.padding = const EdgeInsets.all(16),
this.margin,
this.borderRadius = 16,
this.blur = 10,
this.backgroundColor,
this.borderColor,
});
final Widget child;
final EdgeInsetsGeometry padding;
final EdgeInsetsGeometry? margin;
final double borderRadius;
final double blur;
final Color? backgroundColor;
final Color? borderColor;
@override
Widget build(BuildContext context) {
final effectiveBackground =
backgroundColor ?? const Color(0xFF1E293B).withOpacity(0.6);
final effectiveBorder =
borderColor ?? Colors.white.withOpacity(0.08);
Widget card = ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
child: Container(
padding: padding,
decoration: BoxDecoration(
color: effectiveBackground,
borderRadius: BorderRadius.circular(borderRadius),
border: Border.all(color: effectiveBorder, width: 1),
),
child: child,
),
),
);
if (margin != null) {
return Container(margin: margin, child: card);
}
return card;
}
}

View File

@@ -0,0 +1,117 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
class TierAvatarRing extends StatelessWidget {
final String username;
final String tier;
final double size;
final bool showDiceBear;
final String? imageUrl;
final VoidCallback? onTap;
static const Map<String, Color> _tierColors = {
'Bronze': Color(0xFFFED7AA),
'Silver': Color(0xFFE2E8F0),
'Gold': Color(0xFFFEF3C7),
'Platinum': Color(0xFFEDE9FE),
'Diamond': Color(0xFFE0E7FF),
};
static const Color _fallbackColor = Color(0xFF475569);
const TierAvatarRing({
super.key,
required this.username,
required this.tier,
this.size = 48.0,
this.showDiceBear = true,
this.imageUrl,
this.onTap,
});
Color get _ringColor => _tierColors[tier] ?? _fallbackColor;
String get _avatarUrl {
if (imageUrl != null && imageUrl!.isNotEmpty) {
return imageUrl!;
}
return 'https://api.dicebear.com/9.x/notionists/svg?seed=$username';
}
Widget _buildAvatar() {
final double radius = size / 2 - 5;
if (!showDiceBear) {
return CircleAvatar(
radius: radius,
backgroundColor: const Color(0xFF1E293B),
child: Icon(
Icons.person,
color: Colors.white54,
size: size * 0.5,
),
);
}
return CachedNetworkImage(
imageUrl: _avatarUrl,
imageBuilder: (context, imageProvider) => CircleAvatar(
radius: radius,
backgroundImage: imageProvider,
),
placeholder: (context, url) => CircleAvatar(
radius: radius,
backgroundColor: const Color(0xFF1E293B),
child: SizedBox(
width: size * 0.4,
height: size * 0.4,
child: const CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white38,
),
),
),
errorWidget: (context, url, error) => CircleAvatar(
radius: radius,
backgroundColor: const Color(0xFF1E293B),
child: Icon(
Icons.person,
color: Colors.white54,
size: size * 0.5,
),
),
);
}
@override
Widget build(BuildContext context) {
final Color ringColor = _ringColor;
final double containerSize = size + 6;
final Widget ring = Container(
width: containerSize,
height: containerSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: ringColor, width: 3),
boxShadow: [
BoxShadow(
color: ringColor.withOpacity(0.4),
blurRadius: 8,
spreadRadius: 1,
),
],
),
child: Center(child: _buildAvatar()),
);
if (onTap != null) {
return GestureDetector(
onTap: onTap,
child: ring,
);
}
return ring;
}
}