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

@@ -31,6 +31,7 @@ class _CheckoutScreenState extends State<CheckoutScreen> {
final _nameCtrl = TextEditingController();
final _emailCtrl = TextEditingController();
final _phoneCtrl = TextEditingController();
final _promoCtrl = TextEditingController();
@override
void initState() {
@@ -77,6 +78,7 @@ class _CheckoutScreenState extends State<CheckoutScreen> {
_nameCtrl.dispose();
_emailCtrl.dispose();
_phoneCtrl.dispose();
_promoCtrl.dispose();
super.dispose();
}
@@ -253,6 +255,84 @@ class _CheckoutScreenState extends State<CheckoutScreen> {
_field('Full Name', _nameCtrl, validator: (v) => v!.isEmpty ? 'Required' : null),
_field('Email', _emailCtrl, type: TextInputType.emailAddress, validator: (v) => v!.isEmpty ? 'Required' : null),
_field('Phone', _phoneCtrl, type: TextInputType.phone, validator: (v) => v!.isEmpty ? 'Required' : null),
const SizedBox(height: 8),
Consumer<CheckoutProvider>(
builder: (context, provider, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextFormField(
controller: _promoCtrl,
textCapitalization: TextCapitalization.characters,
decoration: InputDecoration(
labelText: 'Promo Code (optional)',
border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)),
filled: true,
fillColor: Colors.grey.shade50,
suffixIcon: provider.promoApplied
? const Icon(Icons.check_circle, color: Colors.green, size: 20)
: null,
),
enabled: !provider.promoApplied,
),
),
const SizedBox(width: 8),
SizedBox(
height: 56,
child: provider.promoApplied
? OutlinedButton(
onPressed: () {
provider.resetPromo();
_promoCtrl.clear();
},
style: OutlinedButton.styleFrom(foregroundColor: Colors.red),
child: const Text('Remove'),
)
: ElevatedButton(
onPressed: provider.loading
? null
: () async {
final ok = await provider.applyPromo(_promoCtrl.text);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(provider.promoMessage ??
(ok ? 'Promo applied!' : 'Invalid promo code')),
backgroundColor: ok ? Colors.green : Colors.red,
),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0B63D6),
foregroundColor: Colors.white,
),
child: const Text('Apply'),
),
),
],
),
if (provider.promoApplied && provider.promoMessage != null) ...[
const SizedBox(height: 6),
Row(
children: [
const Icon(Icons.local_offer, size: 14, color: Colors.green),
const SizedBox(width: 4),
Text(
'${provider.promoMessage} — saves \u20b9${provider.discountAmount.toStringAsFixed(0)}',
style: const TextStyle(fontSize: 12, color: Colors.green),
),
],
),
],
],
);
},
),
],
),
),
@@ -293,6 +373,34 @@ class _CheckoutScreenState extends State<CheckoutScreen> {
),
)),
const Divider(height: 32),
if (provider.promoApplied) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Subtotal', style: TextStyle(color: Colors.grey.shade700)),
Text('Rs ${provider.subtotal.toStringAsFixed(0)}', style: TextStyle(color: Colors.grey.shade700)),
],
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(Icons.local_offer, size: 14, color: Colors.green),
const SizedBox(width: 4),
Text(
provider.couponCode != null ? 'Promo (${provider.couponCode})' : 'Promo discount',
style: const TextStyle(color: Colors.green),
),
],
),
Text('- Rs ${provider.discountAmount.toStringAsFixed(0)}',
style: const TextStyle(color: Colors.green, fontWeight: FontWeight.w600)),
],
),
const SizedBox(height: 8),
],
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [