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:
@@ -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: [
|
||||
|
||||
Reference in New Issue
Block a user