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:
465
assets/data/kerala_pincodes.json
Normal file
465
assets/data/kerala_pincodes.json
Normal file
@@ -0,0 +1,465 @@
|
|||||||
|
[
|
||||||
|
{"city": "Thiruvananthapuram", "district": "Thiruvananthapuram", "pincode": "695001"},
|
||||||
|
{"city": "Kazhakoottam", "district": "Thiruvananthapuram", "pincode": "695582"},
|
||||||
|
{"city": "Technopark", "district": "Thiruvananthapuram", "pincode": "695581"},
|
||||||
|
{"city": "Varkala", "district": "Thiruvananthapuram", "pincode": "695141"},
|
||||||
|
{"city": "Attingal", "district": "Thiruvananthapuram", "pincode": "695101"},
|
||||||
|
{"city": "Neyyattinkara", "district": "Thiruvananthapuram", "pincode": "695121"},
|
||||||
|
{"city": "Nedumangad", "district": "Thiruvananthapuram", "pincode": "695541"},
|
||||||
|
{"city": "Pothencode", "district": "Thiruvananthapuram", "pincode": "695584"},
|
||||||
|
{"city": "Peroorkada", "district": "Thiruvananthapuram", "pincode": "695005"},
|
||||||
|
{"city": "Kazhakkoottam", "district": "Thiruvananthapuram", "pincode": "695582"},
|
||||||
|
{"city": "Nemom", "district": "Thiruvananthapuram", "pincode": "695020"},
|
||||||
|
{"city": "Kaniyapuram", "district": "Thiruvananthapuram", "pincode": "695301"},
|
||||||
|
{"city": "Balaramapuram", "district": "Thiruvananthapuram", "pincode": "695501"},
|
||||||
|
{"city": "Kallambalam", "district": "Thiruvananthapuram", "pincode": "695605"},
|
||||||
|
{"city": "Chirayinkeezhu", "district": "Thiruvananthapuram", "pincode": "695304"},
|
||||||
|
{"city": "Venjaramoodu", "district": "Thiruvananthapuram", "pincode": "695607"},
|
||||||
|
{"city": "Kattakada", "district": "Thiruvananthapuram", "pincode": "695572"},
|
||||||
|
{"city": "Aryanad", "district": "Thiruvananthapuram", "pincode": "695542"},
|
||||||
|
{"city": "Vellanad", "district": "Thiruvananthapuram", "pincode": "695543"},
|
||||||
|
{"city": "Parassala", "district": "Thiruvananthapuram", "pincode": "695502"},
|
||||||
|
{"city": "Kollam", "district": "Kollam", "pincode": "691001"},
|
||||||
|
{"city": "Karunagappally", "district": "Kollam", "pincode": "690518"},
|
||||||
|
{"city": "Kundara", "district": "Kollam", "pincode": "691501"},
|
||||||
|
{"city": "Punalur", "district": "Kollam", "pincode": "691305"},
|
||||||
|
{"city": "Kottarakkara", "district": "Kollam", "pincode": "691506"},
|
||||||
|
{"city": "Paravur", "district": "Kollam", "pincode": "691301"},
|
||||||
|
{"city": "Chavara", "district": "Kollam", "pincode": "691583"},
|
||||||
|
{"city": "Pathanapuram", "district": "Kollam", "pincode": "689695"},
|
||||||
|
{"city": "Ezhukone", "district": "Kollam", "pincode": "691505"},
|
||||||
|
{"city": "Anchal", "district": "Kollam", "pincode": "691306"},
|
||||||
|
{"city": "Kottamkara", "district": "Kollam", "pincode": "691007"},
|
||||||
|
{"city": "Perinad", "district": "Kollam", "pincode": "691601"},
|
||||||
|
{"city": "Kulathupuzha", "district": "Kollam", "pincode": "691310"},
|
||||||
|
{"city": "Ittiva", "district": "Kollam", "pincode": "691311"},
|
||||||
|
{"city": "Mynagappally", "district": "Kollam", "pincode": "690503"},
|
||||||
|
{"city": "Pathanamthitta", "district": "Pathanamthitta", "pincode": "689645"},
|
||||||
|
{"city": "Adoor", "district": "Pathanamthitta", "pincode": "691523"},
|
||||||
|
{"city": "Thiruvalla", "district": "Pathanamthitta", "pincode": "689101"},
|
||||||
|
{"city": "Pandalam", "district": "Pathanamthitta", "pincode": "689501"},
|
||||||
|
{"city": "Ranni", "district": "Pathanamthitta", "pincode": "689673"},
|
||||||
|
{"city": "Kozhencherry", "district": "Pathanamthitta", "pincode": "689641"},
|
||||||
|
{"city": "Konni", "district": "Pathanamthitta", "pincode": "689691"},
|
||||||
|
{"city": "Mallapally", "district": "Pathanamthitta", "pincode": "686501"},
|
||||||
|
{"city": "Tiruvalla", "district": "Pathanamthitta", "pincode": "689101"},
|
||||||
|
{"city": "Aranmula", "district": "Pathanamthitta", "pincode": "689533"},
|
||||||
|
{"city": "Angamaly", "district": "Pathanamthitta", "pincode": "686540"},
|
||||||
|
{"city": "Alappuzha", "district": "Alappuzha", "pincode": "688001"},
|
||||||
|
{"city": "Cherthala", "district": "Alappuzha", "pincode": "688524"},
|
||||||
|
{"city": "Kayamkulam", "district": "Alappuzha", "pincode": "690502"},
|
||||||
|
{"city": "Mavelikkara", "district": "Alappuzha", "pincode": "690101"},
|
||||||
|
{"city": "Haripad", "district": "Alappuzha", "pincode": "690514"},
|
||||||
|
{"city": "Mararikulam", "district": "Alappuzha", "pincode": "688523"},
|
||||||
|
{"city": "Ambalapuzha", "district": "Alappuzha", "pincode": "688561"},
|
||||||
|
{"city": "Kuttanad", "district": "Alappuzha", "pincode": "688501"},
|
||||||
|
{"city": "Mannar", "district": "Alappuzha", "pincode": "689622"},
|
||||||
|
{"city": "Chengannur", "district": "Alappuzha", "pincode": "689121"},
|
||||||
|
{"city": "Purakkad", "district": "Alappuzha", "pincode": "688551"},
|
||||||
|
{"city": "Aroor", "district": "Alappuzha", "pincode": "688534"},
|
||||||
|
{"city": "Kottayam", "district": "Kottayam", "pincode": "686001"},
|
||||||
|
{"city": "Changanacherry", "district": "Kottayam", "pincode": "686101"},
|
||||||
|
{"city": "Pala", "district": "Kottayam", "pincode": "686575"},
|
||||||
|
{"city": "Ettumanoor", "district": "Kottayam", "pincode": "686631"},
|
||||||
|
{"city": "Vaikom", "district": "Kottayam", "pincode": "686141"},
|
||||||
|
{"city": "Kanjirappally", "district": "Kottayam", "pincode": "686507"},
|
||||||
|
{"city": "Erattupetta", "district": "Kottayam", "pincode": "686121"},
|
||||||
|
{"city": "Ponkunnam", "district": "Kottayam", "pincode": "686506"},
|
||||||
|
{"city": "Karukachal", "district": "Kottayam", "pincode": "686540"},
|
||||||
|
{"city": "Kaduthuruthy", "district": "Kottayam", "pincode": "686604"},
|
||||||
|
{"city": "Meenachil", "district": "Kottayam", "pincode": "686516"},
|
||||||
|
{"city": "Thodupuzha", "district": "Idukki", "pincode": "685584"},
|
||||||
|
{"city": "Idukki", "district": "Idukki", "pincode": "685602"},
|
||||||
|
{"city": "Munnar", "district": "Idukki", "pincode": "685612"},
|
||||||
|
{"city": "Kattappana", "district": "Idukki", "pincode": "685508"},
|
||||||
|
{"city": "Adimali", "district": "Idukki", "pincode": "685561"},
|
||||||
|
{"city": "Kumily", "district": "Idukki", "pincode": "685509"},
|
||||||
|
{"city": "Devikulam", "district": "Idukki", "pincode": "685613"},
|
||||||
|
{"city": "Peermedu", "district": "Idukki", "pincode": "685531"},
|
||||||
|
{"city": "Nedumkandam", "district": "Idukki", "pincode": "685553"},
|
||||||
|
{"city": "Rajakkad", "district": "Idukki", "pincode": "685505"},
|
||||||
|
{"city": "Ernakulam", "district": "Ernakulam", "pincode": "682001"},
|
||||||
|
{"city": "Kochi", "district": "Ernakulam", "pincode": "682001"},
|
||||||
|
{"city": "Cochin", "district": "Ernakulam", "pincode": "682001"},
|
||||||
|
{"city": "Aluva", "district": "Ernakulam", "pincode": "683101"},
|
||||||
|
{"city": "Angamaly", "district": "Ernakulam", "pincode": "683572"},
|
||||||
|
{"city": "Perumbavoor", "district": "Ernakulam", "pincode": "683542"},
|
||||||
|
{"city": "Muvattupuzha", "district": "Ernakulam", "pincode": "686661"},
|
||||||
|
{"city": "Kalady", "district": "Ernakulam", "pincode": "683574"},
|
||||||
|
{"city": "Kothamangalam", "district": "Ernakulam", "pincode": "686691"},
|
||||||
|
{"city": "Kakkanad", "district": "Ernakulam", "pincode": "682030"},
|
||||||
|
{"city": "Edappally", "district": "Ernakulam", "pincode": "682024"},
|
||||||
|
{"city": "Kalamassery", "district": "Ernakulam", "pincode": "683104"},
|
||||||
|
{"city": "Fort Kochi", "district": "Ernakulam", "pincode": "682001"},
|
||||||
|
{"city": "Vypeen", "district": "Ernakulam", "pincode": "683515"},
|
||||||
|
{"city": "Cherai", "district": "Ernakulam", "pincode": "683514"},
|
||||||
|
{"city": "Paravur", "district": "Ernakulam", "pincode": "683513"},
|
||||||
|
{"city": "Palluruthy", "district": "Ernakulam", "pincode": "682006"},
|
||||||
|
{"city": "Mattancherry", "district": "Ernakulam", "pincode": "682002"},
|
||||||
|
{"city": "Ponnurunni", "district": "Ernakulam", "pincode": "682019"},
|
||||||
|
{"city": "Maradu", "district": "Ernakulam", "pincode": "682304"},
|
||||||
|
{"city": "Thrikkakara", "district": "Ernakulam", "pincode": "682021"},
|
||||||
|
{"city": "Vyttila", "district": "Ernakulam", "pincode": "682019"},
|
||||||
|
{"city": "Palarivattom", "district": "Ernakulam", "pincode": "682025"},
|
||||||
|
{"city": "Panangad", "district": "Ernakulam", "pincode": "682506"},
|
||||||
|
{"city": "Cheranalloor", "district": "Ernakulam", "pincode": "682034"},
|
||||||
|
{"city": "Poothrikka", "district": "Ernakulam", "pincode": "683517"},
|
||||||
|
{"city": "Manjapra", "district": "Ernakulam", "pincode": "683581"},
|
||||||
|
{"city": "Mulanthuruthy", "district": "Ernakulam", "pincode": "682314"},
|
||||||
|
{"city": "Thrissur", "district": "Thrissur", "pincode": "680001"},
|
||||||
|
{"city": "Irinjalakuda", "district": "Thrissur", "pincode": "680121"},
|
||||||
|
{"city": "Chalakudy", "district": "Thrissur", "pincode": "680307"},
|
||||||
|
{"city": "Guruvayur", "district": "Thrissur", "pincode": "680101"},
|
||||||
|
{"city": "Kodungallur", "district": "Thrissur", "pincode": "680664"},
|
||||||
|
{"city": "Kunnamkulam", "district": "Thrissur", "pincode": "680503"},
|
||||||
|
{"city": "Chavakkad", "district": "Thrissur", "pincode": "680506"},
|
||||||
|
{"city": "Wadakkanchery", "district": "Thrissur", "pincode": "680582"},
|
||||||
|
{"city": "Trichur", "district": "Thrissur", "pincode": "680001"},
|
||||||
|
{"city": "Ollur", "district": "Thrissur", "pincode": "680306"},
|
||||||
|
{"city": "Puthur", "district": "Thrissur", "pincode": "680553"},
|
||||||
|
{"city": "Cherpu", "district": "Thrissur", "pincode": "680561"},
|
||||||
|
{"city": "Mala", "district": "Thrissur", "pincode": "680732"},
|
||||||
|
{"city": "Anthikkad", "district": "Thrissur", "pincode": "680641"},
|
||||||
|
{"city": "Palakkad", "district": "Palakkad", "pincode": "678001"},
|
||||||
|
{"city": "Ottapalam", "district": "Palakkad", "pincode": "679101"},
|
||||||
|
{"city": "Shornur", "district": "Palakkad", "pincode": "679121"},
|
||||||
|
{"city": "Mannarkkad", "district": "Palakkad", "pincode": "678582"},
|
||||||
|
{"city": "Chittur", "district": "Palakkad", "pincode": "678101"},
|
||||||
|
{"city": "Alathur", "district": "Palakkad", "pincode": "678541"},
|
||||||
|
{"city": "Pattambi", "district": "Palakkad", "pincode": "679303"},
|
||||||
|
{"city": "Cherpulassery", "district": "Palakkad", "pincode": "679503"},
|
||||||
|
{"city": "Palakkad Town", "district": "Palakkad", "pincode": "678001"},
|
||||||
|
{"city": "Kuzhalmannam", "district": "Palakkad", "pincode": "678702"},
|
||||||
|
{"city": "Vadakkencherry", "district": "Palakkad", "pincode": "678683"},
|
||||||
|
{"city": "Malampuzha", "district": "Palakkad", "pincode": "678651"},
|
||||||
|
{"city": "Malappuram", "district": "Malappuram", "pincode": "676505"},
|
||||||
|
{"city": "Tirur", "district": "Malappuram", "pincode": "676101"},
|
||||||
|
{"city": "Ponnani", "district": "Malappuram", "pincode": "679577"},
|
||||||
|
{"city": "Manjeri", "district": "Malappuram", "pincode": "676121"},
|
||||||
|
{"city": "Perinthalmanna", "district": "Malappuram", "pincode": "679322"},
|
||||||
|
{"city": "Kondotty", "district": "Malappuram", "pincode": "673638"},
|
||||||
|
{"city": "Tirurrangadi", "district": "Malappuram", "pincode": "676306"},
|
||||||
|
{"city": "Kottakkal", "district": "Malappuram", "pincode": "676503"},
|
||||||
|
{"city": "Nilambur", "district": "Malappuram", "pincode": "679329"},
|
||||||
|
{"city": "Wandoor", "district": "Malappuram", "pincode": "679328"},
|
||||||
|
{"city": "Edappal", "district": "Malappuram", "pincode": "679576"},
|
||||||
|
{"city": "Vengara", "district": "Malappuram", "pincode": "676304"},
|
||||||
|
{"city": "Valanchery", "district": "Malappuram", "pincode": "676552"},
|
||||||
|
{"city": "Pandikkad", "district": "Malappuram", "pincode": "676521"},
|
||||||
|
{"city": "Kozhikode", "district": "Kozhikode", "pincode": "673001"},
|
||||||
|
{"city": "Calicut", "district": "Kozhikode", "pincode": "673001"},
|
||||||
|
{"city": "Vatakara", "district": "Kozhikode", "pincode": "673101"},
|
||||||
|
{"city": "Koyilandy", "district": "Kozhikode", "pincode": "673305"},
|
||||||
|
{"city": "Ramanattukara", "district": "Kozhikode", "pincode": "673633"},
|
||||||
|
{"city": "Feroke", "district": "Kozhikode", "pincode": "673631"},
|
||||||
|
{"city": "Perambra", "district": "Kozhikode", "pincode": "673525"},
|
||||||
|
{"city": "Quilandy", "district": "Kozhikode", "pincode": "673305"},
|
||||||
|
{"city": "Balussery", "district": "Kozhikode", "pincode": "673612"},
|
||||||
|
{"city": "Chelannur", "district": "Kozhikode", "pincode": "673616"},
|
||||||
|
{"city": "Koduvally", "district": "Kozhikode", "pincode": "673572"},
|
||||||
|
{"city": "Nanmanda", "district": "Kozhikode", "pincode": "673613"},
|
||||||
|
{"city": "Thamarassery", "district": "Kozhikode", "pincode": "673573"},
|
||||||
|
{"city": "Ulliyeri", "district": "Kozhikode", "pincode": "673315"},
|
||||||
|
{"city": "Elathur", "district": "Kozhikode", "pincode": "673303"},
|
||||||
|
{"city": "Olavanna", "district": "Kozhikode", "pincode": "673019"},
|
||||||
|
{"city": "Wayanad", "district": "Wayanad", "pincode": "673121"},
|
||||||
|
{"city": "Kalpetta", "district": "Wayanad", "pincode": "673121"},
|
||||||
|
{"city": "Mananthavady", "district": "Wayanad", "pincode": "670645"},
|
||||||
|
{"city": "Sulthan Bathery", "district": "Wayanad", "pincode": "673592"},
|
||||||
|
{"city": "Ambalavayal", "district": "Wayanad", "pincode": "673593"},
|
||||||
|
{"city": "Vythiri", "district": "Wayanad", "pincode": "673576"},
|
||||||
|
{"city": "Panamaram", "district": "Wayanad", "pincode": "670721"},
|
||||||
|
{"city": "Meenangadi", "district": "Wayanad", "pincode": "673591"},
|
||||||
|
{"city": "Pulpally", "district": "Wayanad", "pincode": "673579"},
|
||||||
|
{"city": "Meppadi", "district": "Wayanad", "pincode": "673577"},
|
||||||
|
{"city": "Kannur", "district": "Kannur", "pincode": "670001"},
|
||||||
|
{"city": "Thalassery", "district": "Kannur", "pincode": "670101"},
|
||||||
|
{"city": "Payyanur", "district": "Kannur", "pincode": "670307"},
|
||||||
|
{"city": "Iritty", "district": "Kannur", "pincode": "670703"},
|
||||||
|
{"city": "Mattannur", "district": "Kannur", "pincode": "670702"},
|
||||||
|
{"city": "Kuthuparamba", "district": "Kannur", "pincode": "670643"},
|
||||||
|
{"city": "Taliparamba", "district": "Kannur", "pincode": "670141"},
|
||||||
|
{"city": "Anthoor", "district": "Kannur", "pincode": "670306"},
|
||||||
|
{"city": "Peravoor", "district": "Kannur", "pincode": "670673"},
|
||||||
|
{"city": "Sreekandapuram", "district": "Kannur", "pincode": "670631"},
|
||||||
|
{"city": "Payyavoor", "district": "Kannur", "pincode": "670693"},
|
||||||
|
{"city": "Kasaragod", "district": "Kasaragod", "pincode": "671121"},
|
||||||
|
{"city": "Kanhangad", "district": "Kasaragod", "pincode": "671315"},
|
||||||
|
{"city": "Manjeshwar", "district": "Kasaragod", "pincode": "671323"},
|
||||||
|
{"city": "Hosdurg", "district": "Kasaragod", "pincode": "671315"},
|
||||||
|
{"city": "Nileshwar", "district": "Kasaragod", "pincode": "671314"},
|
||||||
|
{"city": "Cheruvathur", "district": "Kasaragod", "pincode": "671310"},
|
||||||
|
{"city": "Uppala", "district": "Kasaragod", "pincode": "671322"},
|
||||||
|
{"city": "Bekal", "district": "Kasaragod", "pincode": "671318"},
|
||||||
|
{"city": "Edneer", "district": "Kasaragod", "pincode": "671541"},
|
||||||
|
{"city": "Parappa", "district": "Kasaragod", "pincode": "671533"},
|
||||||
|
{"city": "Vivekanandapuram", "district": "Thiruvananthapuram", "pincode": "695003"},
|
||||||
|
{"city": "Kesavadasapuram", "district": "Thiruvananthapuram", "pincode": "695004"},
|
||||||
|
{"city": "Kowdiar", "district": "Thiruvananthapuram", "pincode": "695003"},
|
||||||
|
{"city": "Sasthamangalam", "district": "Thiruvananthapuram", "pincode": "695010"},
|
||||||
|
{"city": "Palayam", "district": "Thiruvananthapuram", "pincode": "695034"},
|
||||||
|
{"city": "Vazhuthacaud", "district": "Thiruvananthapuram", "pincode": "695014"},
|
||||||
|
{"city": "Mannanthala", "district": "Thiruvananthapuram", "pincode": "695015"},
|
||||||
|
{"city": "Karamana", "district": "Thiruvananthapuram", "pincode": "695002"},
|
||||||
|
{"city": "Enchakkal", "district": "Thiruvananthapuram", "pincode": "695615"},
|
||||||
|
{"city": "Sreekaryam", "district": "Thiruvananthapuram", "pincode": "695017"},
|
||||||
|
{"city": "Ulloor", "district": "Thiruvananthapuram", "pincode": "695011"},
|
||||||
|
{"city": "Medical College", "district": "Thiruvananthapuram", "pincode": "695011"},
|
||||||
|
{"city": "Nalanchira", "district": "Thiruvananthapuram", "pincode": "695015"},
|
||||||
|
{"city": "Vellayambalam", "district": "Thiruvananthapuram", "pincode": "695010"},
|
||||||
|
{"city": "Ambalamukku", "district": "Thiruvananthapuram", "pincode": "695005"},
|
||||||
|
{"city": "Ookode", "district": "Thiruvananthapuram", "pincode": "695616"},
|
||||||
|
{"city": "Kariavattom", "district": "Thiruvananthapuram", "pincode": "695581"},
|
||||||
|
{"city": "Aakkulam", "district": "Thiruvananthapuram", "pincode": "695011"},
|
||||||
|
{"city": "Pallichal", "district": "Thiruvananthapuram", "pincode": "695020"},
|
||||||
|
{"city": "Thirumala", "district": "Thiruvananthapuram", "pincode": "695006"},
|
||||||
|
{"city": "Thiruvanikkulam", "district": "Ernakulam", "pincode": "682305"},
|
||||||
|
{"city": "Kumbalangi", "district": "Ernakulam", "pincode": "682007"},
|
||||||
|
{"city": "Thevara", "district": "Ernakulam", "pincode": "682013"},
|
||||||
|
{"city": "Kadavanthra", "district": "Ernakulam", "pincode": "682020"},
|
||||||
|
{"city": "Kacheripady", "district": "Ernakulam", "pincode": "682018"},
|
||||||
|
{"city": "Gandhinagar", "district": "Ernakulam", "pincode": "682020"},
|
||||||
|
{"city": "Vallarpadam", "district": "Ernakulam", "pincode": "683112"},
|
||||||
|
{"city": "Willingdon Island", "district": "Ernakulam", "pincode": "682003"},
|
||||||
|
{"city": "Mulavukad", "district": "Ernakulam", "pincode": "682504"},
|
||||||
|
{"city": "Aroor", "district": "Ernakulam", "pincode": "688534"},
|
||||||
|
{"city": "Kumbalam", "district": "Ernakulam", "pincode": "682507"},
|
||||||
|
{"city": "Chottanikkara", "district": "Ernakulam", "pincode": "682312"},
|
||||||
|
{"city": "Kolenchery", "district": "Ernakulam", "pincode": "682311"},
|
||||||
|
{"city": "Kizhakkambalam", "district": "Ernakulam", "pincode": "683562"},
|
||||||
|
{"city": "Paravur North", "district": "Ernakulam", "pincode": "683513"},
|
||||||
|
{"city": "Varappuzha", "district": "Ernakulam", "pincode": "683517"},
|
||||||
|
{"city": "Vadavucode", "district": "Ernakulam", "pincode": "682023"},
|
||||||
|
{"city": "Karuvelipady", "district": "Ernakulam", "pincode": "682005"},
|
||||||
|
{"city": "Manjaly", "district": "Ernakulam", "pincode": "683580"},
|
||||||
|
{"city": "Koratty", "district": "Thrissur", "pincode": "680308"},
|
||||||
|
{"city": "Puzhakkal", "district": "Thrissur", "pincode": "680553"},
|
||||||
|
{"city": "Thrissur Town", "district": "Thrissur", "pincode": "680020"},
|
||||||
|
{"city": "Viyyur", "district": "Thrissur", "pincode": "680010"},
|
||||||
|
{"city": "Pookattupady", "district": "Thrissur", "pincode": "683577"},
|
||||||
|
{"city": "Ashtamichira", "district": "Thrissur", "pincode": "680732"},
|
||||||
|
{"city": "Karalam", "district": "Thrissur", "pincode": "680702"},
|
||||||
|
{"city": "Erumappetty", "district": "Thrissur", "pincode": "680642"},
|
||||||
|
{"city": "Pariyaram", "district": "Thrissur", "pincode": "680724"},
|
||||||
|
{"city": "Mulankunnathukavu", "district": "Thrissur", "pincode": "680581"},
|
||||||
|
{"city": "Peringottukara", "district": "Thrissur", "pincode": "680586"},
|
||||||
|
{"city": "Shoranur Town", "district": "Palakkad", "pincode": "679122"},
|
||||||
|
{"city": "Kalpathy", "district": "Palakkad", "pincode": "678003"},
|
||||||
|
{"city": "Kollengode", "district": "Palakkad", "pincode": "678506"},
|
||||||
|
{"city": "Palakkad Fort", "district": "Palakkad", "pincode": "678001"},
|
||||||
|
{"city": "Nemmara", "district": "Palakkad", "pincode": "678508"},
|
||||||
|
{"city": "Kongad", "district": "Palakkad", "pincode": "678631"},
|
||||||
|
{"city": "Ayilur", "district": "Palakkad", "pincode": "679514"},
|
||||||
|
{"city": "Parali", "district": "Palakkad", "pincode": "679517"},
|
||||||
|
{"city": "Agali", "district": "Palakkad", "pincode": "678581"},
|
||||||
|
{"city": "Mannarkkad Town", "district": "Palakkad", "pincode": "678583"},
|
||||||
|
{"city": "Tenhipalam", "district": "Malappuram", "pincode": "673636"},
|
||||||
|
{"city": "Calicut University", "district": "Malappuram", "pincode": "673635"},
|
||||||
|
{"city": "Tirur Town", "district": "Malappuram", "pincode": "676102"},
|
||||||
|
{"city": "Manjeri Town", "district": "Malappuram", "pincode": "676122"},
|
||||||
|
{"city": "Parappanangadi", "district": "Malappuram", "pincode": "676303"},
|
||||||
|
{"city": "Areekode", "district": "Malappuram", "pincode": "673639"},
|
||||||
|
{"city": "Karuvarakundu", "district": "Malappuram", "pincode": "673523"},
|
||||||
|
{"city": "Malappuram Town", "district": "Malappuram", "pincode": "676506"},
|
||||||
|
{"city": "Pottasseri", "district": "Malappuram", "pincode": "679321"},
|
||||||
|
{"city": "Kolathur", "district": "Malappuram", "pincode": "676103"},
|
||||||
|
{"city": "Kottappuram", "district": "Thrissur", "pincode": "680663"},
|
||||||
|
{"city": "Triprayar", "district": "Thrissur", "pincode": "680566"},
|
||||||
|
{"city": "Pavaratty", "district": "Thrissur", "pincode": "680507"},
|
||||||
|
{"city": "Mathilakam", "district": "Thrissur", "pincode": "680686"},
|
||||||
|
{"city": "Pazhayannur", "district": "Thrissur", "pincode": "679533"},
|
||||||
|
{"city": "Kannayi", "district": "Kannur", "pincode": "670003"},
|
||||||
|
{"city": "Chirakkal", "district": "Kannur", "pincode": "670011"},
|
||||||
|
{"city": "Dharmadam", "district": "Kannur", "pincode": "670005"},
|
||||||
|
{"city": "Pappinisseri", "district": "Kannur", "pincode": "670561"},
|
||||||
|
{"city": "Chovva", "district": "Kannur", "pincode": "670006"},
|
||||||
|
{"city": "Thana", "district": "Kannur", "pincode": "670671"},
|
||||||
|
{"city": "Madayi", "district": "Kannur", "pincode": "670302"},
|
||||||
|
{"city": "Payyoli", "district": "Kozhikode", "pincode": "673522"},
|
||||||
|
{"city": "Kuttiyadi", "district": "Kozhikode", "pincode": "673508"},
|
||||||
|
{"city": "Beypore", "district": "Kozhikode", "pincode": "673015"},
|
||||||
|
{"city": "Calicut Beach", "district": "Kozhikode", "pincode": "673032"},
|
||||||
|
{"city": "Nadapuram", "district": "Kozhikode", "pincode": "673504"},
|
||||||
|
{"city": "Thodannur", "district": "Kozhikode", "pincode": "673307"},
|
||||||
|
{"city": "Kalpetta Town", "district": "Wayanad", "pincode": "673122"},
|
||||||
|
{"city": "Pulpally Town", "district": "Wayanad", "pincode": "673578"},
|
||||||
|
{"city": "Kuppadi", "district": "Wayanad", "pincode": "673596"},
|
||||||
|
{"city": "Noolpuzha", "district": "Wayanad", "pincode": "673594"},
|
||||||
|
{"city": "Thrissur City", "district": "Thrissur", "pincode": "680022"},
|
||||||
|
{"city": "Ollur Town", "district": "Thrissur", "pincode": "680311"},
|
||||||
|
{"city": "Velur", "district": "Thrissur", "pincode": "680601"},
|
||||||
|
{"city": "Chazhur", "district": "Thrissur", "pincode": "680613"},
|
||||||
|
{"city": "Mannuthy", "district": "Thrissur", "pincode": "680651"},
|
||||||
|
{"city": "Kottuvally", "district": "Ernakulam", "pincode": "686663"},
|
||||||
|
{"city": "Puthencruz", "district": "Ernakulam", "pincode": "682308"},
|
||||||
|
{"city": "Thalayolaparambu", "district": "Kottayam", "pincode": "686605"},
|
||||||
|
{"city": "Nattakom", "district": "Kottayam", "pincode": "686013"},
|
||||||
|
{"city": "Kumarakom", "district": "Kottayam", "pincode": "686563"},
|
||||||
|
{"city": "Kidangoor", "district": "Kottayam", "pincode": "686542"},
|
||||||
|
{"city": "Kottayam City", "district": "Kottayam", "pincode": "686002"},
|
||||||
|
{"city": "Ettumanoor Town", "district": "Kottayam", "pincode": "686632"},
|
||||||
|
{"city": "Uzhavoor", "district": "Kottayam", "pincode": "686634"},
|
||||||
|
{"city": "Athirampuzha", "district": "Kottayam", "pincode": "686562"},
|
||||||
|
{"city": "Lalam", "district": "Idukki", "pincode": "685505"},
|
||||||
|
{"city": "Erattayar", "district": "Idukki", "pincode": "685508"},
|
||||||
|
{"city": "Marayoor", "district": "Idukki", "pincode": "685620"},
|
||||||
|
{"city": "Udumbanchola", "district": "Idukki", "pincode": "685550"},
|
||||||
|
{"city": "Vagamon", "district": "Idukki", "pincode": "685503"},
|
||||||
|
{"city": "Painavu", "district": "Idukki", "pincode": "685603"},
|
||||||
|
{"city": "Kollam City", "district": "Kollam", "pincode": "691013"},
|
||||||
|
{"city": "Thevally", "district": "Kollam", "pincode": "691009"},
|
||||||
|
{"city": "Asramam", "district": "Kollam", "pincode": "691002"},
|
||||||
|
{"city": "Chinnakada", "district": "Kollam", "pincode": "691001"},
|
||||||
|
{"city": "Sakthikulangara", "district": "Kollam", "pincode": "691581"},
|
||||||
|
{"city": "Thangassery", "district": "Kollam", "pincode": "691008"},
|
||||||
|
{"city": "Eravipuram", "district": "Kollam", "pincode": "691021"},
|
||||||
|
{"city": "Neendakara", "district": "Kollam", "pincode": "691582"},
|
||||||
|
{"city": "Thrikkadavathu", "district": "Pathanamthitta", "pincode": "689674"},
|
||||||
|
{"city": "Manarcaud", "district": "Pathanamthitta", "pincode": "689502"},
|
||||||
|
{"city": "Kadapra", "district": "Pathanamthitta", "pincode": "689643"},
|
||||||
|
{"city": "Kulanada", "district": "Pathanamthitta", "pincode": "689503"},
|
||||||
|
{"city": "Niranam", "district": "Pathanamthitta", "pincode": "689621"},
|
||||||
|
{"city": "Venmony", "district": "Alappuzha", "pincode": "689544"},
|
||||||
|
{"city": "Kainakary", "district": "Alappuzha", "pincode": "688504"},
|
||||||
|
{"city": "Kalavoor", "district": "Alappuzha", "pincode": "688522"},
|
||||||
|
{"city": "Thanneermukkom", "district": "Alappuzha", "pincode": "688527"},
|
||||||
|
{"city": "Thycattussery", "district": "Alappuzha", "pincode": "688526"},
|
||||||
|
{"city": "Kanichukulangara", "district": "Alappuzha", "pincode": "688522"},
|
||||||
|
{"city": "Kasaragod Town", "district": "Kasaragod", "pincode": "671122"},
|
||||||
|
{"city": "Kanhangad Town", "district": "Kasaragod", "pincode": "671316"},
|
||||||
|
{"city": "Badiadka", "district": "Kasaragod", "pincode": "671551"},
|
||||||
|
{"city": "Vellarikundu", "district": "Kasaragod", "pincode": "671348"},
|
||||||
|
{"city": "Trikaripur", "district": "Kasaragod", "pincode": "671310"},
|
||||||
|
{"city": "Chandera", "district": "Kasaragod", "pincode": "671541"},
|
||||||
|
{"city": "Mulleria", "district": "Kasaragod", "pincode": "671542"},
|
||||||
|
{"city": "Periya", "district": "Kasaragod", "pincode": "671543"},
|
||||||
|
{"city": "Manjeswaram", "district": "Kasaragod", "pincode": "671323"},
|
||||||
|
{"city": "Mavoor", "district": "Kozhikode", "pincode": "673661"},
|
||||||
|
{"city": "Kappad", "district": "Kozhikode", "pincode": "673521"},
|
||||||
|
{"city": "Kadalundi", "district": "Kozhikode", "pincode": "673302"},
|
||||||
|
{"city": "Thiruvambady", "district": "Kozhikode", "pincode": "673603"},
|
||||||
|
{"city": "Kuttiady", "district": "Kozhikode", "pincode": "673506"},
|
||||||
|
{"city": "Meppayur", "district": "Kozhikode", "pincode": "673524"},
|
||||||
|
{"city": "Atholi", "district": "Kozhikode", "pincode": "673315"},
|
||||||
|
{"city": "Chelembra", "district": "Malappuram", "pincode": "673634"},
|
||||||
|
{"city": "Tirur Junction", "district": "Malappuram", "pincode": "676103"},
|
||||||
|
{"city": "Tanur", "district": "Malappuram", "pincode": "676302"},
|
||||||
|
{"city": "Kuttippuram", "district": "Malappuram", "pincode": "679571"},
|
||||||
|
{"city": "Vettom", "district": "Malappuram", "pincode": "676551"},
|
||||||
|
{"city": "Tirurangadi", "district": "Malappuram", "pincode": "676306"},
|
||||||
|
{"city": "Munderi", "district": "Malappuram", "pincode": "673641"},
|
||||||
|
{"city": "Iringal", "district": "Kozhikode", "pincode": "673521"},
|
||||||
|
{"city": "Koyilandy Town", "district": "Kozhikode", "pincode": "673306"},
|
||||||
|
{"city": "Panniankara", "district": "Kozhikode", "pincode": "673020"},
|
||||||
|
{"city": "Changuvetty", "district": "Kozhikode", "pincode": "673009"},
|
||||||
|
{"city": "West Hill", "district": "Kozhikode", "pincode": "673005"},
|
||||||
|
{"city": "Azhiyur", "district": "Kozhikode", "pincode": "673309"},
|
||||||
|
{"city": "Puthige", "district": "Kasaragod", "pincode": "671124"},
|
||||||
|
{"city": "Kallar", "district": "Idukki", "pincode": "685552"},
|
||||||
|
{"city": "Vandanmedu", "district": "Idukki", "pincode": "685533"},
|
||||||
|
{"city": "Mankulam", "district": "Idukki", "pincode": "685615"},
|
||||||
|
{"city": "Pooppara", "district": "Idukki", "pincode": "685580"},
|
||||||
|
{"city": "Keerithodu", "district": "Idukki", "pincode": "686508"},
|
||||||
|
{"city": "Sreekantapuram", "district": "Kannur", "pincode": "670632"},
|
||||||
|
{"city": "Alakode", "district": "Kannur", "pincode": "670571"},
|
||||||
|
{"city": "Ezhome", "district": "Kannur", "pincode": "670334"},
|
||||||
|
{"city": "Kottiyoor", "district": "Kannur", "pincode": "670731"},
|
||||||
|
{"city": "Irikkur", "district": "Kannur", "pincode": "670592"},
|
||||||
|
{"city": "Arakkal", "district": "Kannur", "pincode": "670301"},
|
||||||
|
{"city": "Ramanthali", "district": "Kannur", "pincode": "670602"},
|
||||||
|
{"city": "Panoor", "district": "Kannur", "pincode": "670692"},
|
||||||
|
{"city": "Calicut University Campus", "district": "Malappuram", "pincode": "673635"},
|
||||||
|
{"city": "Thenjipalam", "district": "Malappuram", "pincode": "673637"},
|
||||||
|
{"city": "Irimbiliyam", "district": "Malappuram", "pincode": "676552"},
|
||||||
|
{"city": "Kuzhimanna", "district": "Malappuram", "pincode": "676501"},
|
||||||
|
{"city": "Edavanna", "district": "Malappuram", "pincode": "676542"},
|
||||||
|
{"city": "Kolathur Town", "district": "Malappuram", "pincode": "676103"},
|
||||||
|
{"city": "Pulamanthole", "district": "Malappuram", "pincode": "679323"},
|
||||||
|
{"city": "Angadippuram", "district": "Malappuram", "pincode": "679321"},
|
||||||
|
{"city": "Perumpadappu", "district": "Malappuram", "pincode": "679579"},
|
||||||
|
{"city": "Thurakkal", "district": "Malappuram", "pincode": "676509"},
|
||||||
|
{"city": "Kallachi", "district": "Kozhikode", "pincode": "673507"},
|
||||||
|
{"city": "Thiruvallur", "district": "Ernakulam", "pincode": "682013"},
|
||||||
|
{"city": "Elamakkara", "district": "Ernakulam", "pincode": "682026"},
|
||||||
|
{"city": "Periyar Nagar", "district": "Ernakulam", "pincode": "683102"},
|
||||||
|
{"city": "Njarakkal", "district": "Ernakulam", "pincode": "683503"},
|
||||||
|
{"city": "Munambam", "district": "Ernakulam", "pincode": "683513"},
|
||||||
|
{"city": "Cherai Beach", "district": "Ernakulam", "pincode": "683514"},
|
||||||
|
{"city": "Puthenvelikkara", "district": "Ernakulam", "pincode": "683594"},
|
||||||
|
{"city": "Valayanchirangara", "district": "Ernakulam", "pincode": "683556"},
|
||||||
|
{"city": "Kizhillam", "district": "Ernakulam", "pincode": "683547"},
|
||||||
|
{"city": "Moovattupuzha", "district": "Ernakulam", "pincode": "686662"},
|
||||||
|
{"city": "Malayattoor", "district": "Ernakulam", "pincode": "683587"},
|
||||||
|
{"city": "Alangad", "district": "Ernakulam", "pincode": "683511"},
|
||||||
|
{"city": "Kadungalloor", "district": "Ernakulam", "pincode": "683110"},
|
||||||
|
{"city": "Piravom", "district": "Ernakulam", "pincode": "686664"},
|
||||||
|
{"city": "Kunnathunad", "district": "Ernakulam", "pincode": "686691"},
|
||||||
|
{"city": "Udayamperoor", "district": "Ernakulam", "pincode": "682307"},
|
||||||
|
{"city": "Vennala", "district": "Ernakulam", "pincode": "682028"},
|
||||||
|
{"city": "Vaduthala", "district": "Ernakulam", "pincode": "682023"},
|
||||||
|
{"city": "Pallikara", "district": "Ernakulam", "pincode": "683565"},
|
||||||
|
{"city": "Keezhmad", "district": "Ernakulam", "pincode": "683583"},
|
||||||
|
{"city": "Kottuvally Town", "district": "Ernakulam", "pincode": "686664"},
|
||||||
|
{"city": "Ramamangalam", "district": "Ernakulam", "pincode": "686663"},
|
||||||
|
{"city": "Karumalloor", "district": "Ernakulam", "pincode": "683576"},
|
||||||
|
{"city": "Manjapra Town", "district": "Ernakulam", "pincode": "683582"},
|
||||||
|
{"city": "Pambady", "district": "Kottayam", "pincode": "686502"},
|
||||||
|
{"city": "Chempu", "district": "Kottayam", "pincode": "686587"},
|
||||||
|
{"city": "Kottayam Junction", "district": "Kottayam", "pincode": "686003"},
|
||||||
|
{"city": "Baker Hill", "district": "Kottayam", "pincode": "686001"},
|
||||||
|
{"city": "Pallom", "district": "Kottayam", "pincode": "686007"},
|
||||||
|
{"city": "Arpookara", "district": "Kottayam", "pincode": "686008"},
|
||||||
|
{"city": "Kudamaloor", "district": "Kottayam", "pincode": "686562"},
|
||||||
|
{"city": "Peroor", "district": "Kottayam", "pincode": "686545"},
|
||||||
|
{"city": "Changanacherry Town", "district": "Kottayam", "pincode": "686102"},
|
||||||
|
{"city": "Vakathanam", "district": "Kottayam", "pincode": "686538"},
|
||||||
|
{"city": "Nattassery", "district": "Kottayam", "pincode": "686535"},
|
||||||
|
{"city": "Karoor", "district": "Kottayam", "pincode": "686544"},
|
||||||
|
{"city": "Manarcad", "district": "Kottayam", "pincode": "686543"},
|
||||||
|
{"city": "Kumarakom Beach", "district": "Kottayam", "pincode": "686563"},
|
||||||
|
{"city": "Alappuzha Beach", "district": "Alappuzha", "pincode": "688012"},
|
||||||
|
{"city": "Alappuzha North", "district": "Alappuzha", "pincode": "688001"},
|
||||||
|
{"city": "Thuravoor", "district": "Alappuzha", "pincode": "688534"},
|
||||||
|
{"city": "Thakazhi", "district": "Alappuzha", "pincode": "688562"},
|
||||||
|
{"city": "Chambakkulam", "district": "Alappuzha", "pincode": "688538"},
|
||||||
|
{"city": "Muttar", "district": "Alappuzha", "pincode": "689623"},
|
||||||
|
{"city": "Aryad", "district": "Alappuzha", "pincode": "688536"},
|
||||||
|
{"city": "Punnapra", "district": "Alappuzha", "pincode": "688004"},
|
||||||
|
{"city": "Pathirapally", "district": "Alappuzha", "pincode": "689652"},
|
||||||
|
{"city": "Pallippad", "district": "Alappuzha", "pincode": "690515"},
|
||||||
|
{"city": "Thiruvalla Town", "district": "Pathanamthitta", "pincode": "689102"},
|
||||||
|
{"city": "Pathanamthitta Town", "district": "Pathanamthitta", "pincode": "689646"},
|
||||||
|
{"city": "Adoor Town", "district": "Pathanamthitta", "pincode": "691524"},
|
||||||
|
{"city": "Pandalam Town", "district": "Pathanamthitta", "pincode": "689502"},
|
||||||
|
{"city": "Elanthoor", "district": "Pathanamthitta", "pincode": "689643"},
|
||||||
|
{"city": "Naranganam", "district": "Pathanamthitta", "pincode": "686535"},
|
||||||
|
{"city": "Mezhuveli", "district": "Pathanamthitta", "pincode": "689648"},
|
||||||
|
{"city": "Murickassery", "district": "Idukki", "pincode": "685512"},
|
||||||
|
{"city": "Thankamani", "district": "Idukki", "pincode": "685509"},
|
||||||
|
{"city": "Upputhara", "district": "Idukki", "pincode": "685505"},
|
||||||
|
{"city": "Vandiperiyar", "district": "Idukki", "pincode": "685533"},
|
||||||
|
{"city": "Kanjikuzhy", "district": "Idukki", "pincode": "686607"},
|
||||||
|
{"city": "Karimannoor", "district": "Idukki", "pincode": "686541"},
|
||||||
|
{"city": "Myladumpara", "district": "Idukki", "pincode": "685515"},
|
||||||
|
{"city": "Vathikudy", "district": "Idukki", "pincode": "685514"},
|
||||||
|
{"city": "Kothamangalam Town", "district": "Ernakulam", "pincode": "686692"},
|
||||||
|
{"city": "Periyapuram", "district": "Thrissur", "pincode": "680025"},
|
||||||
|
{"city": "Ollur Junction", "district": "Thrissur", "pincode": "680307"},
|
||||||
|
{"city": "Kunnamkulam Town", "district": "Thrissur", "pincode": "680504"},
|
||||||
|
{"city": "Guruvayur Town", "district": "Thrissur", "pincode": "680102"},
|
||||||
|
{"city": "Irinjalakuda Town", "district": "Thrissur", "pincode": "680122"},
|
||||||
|
{"city": "Chalakudy Town", "district": "Thrissur", "pincode": "680308"},
|
||||||
|
{"city": "Kodungallur Town", "district": "Thrissur", "pincode": "680665"},
|
||||||
|
{"city": "Kanjani", "district": "Thrissur", "pincode": "680571"},
|
||||||
|
{"city": "Puthukkad", "district": "Thrissur", "pincode": "680301"},
|
||||||
|
{"city": "Moothakunnam", "district": "Ernakulam", "pincode": "683516"},
|
||||||
|
{"city": "Nayarambalam", "district": "Ernakulam", "pincode": "682509"},
|
||||||
|
{"city": "Chendamangalam", "district": "Ernakulam", "pincode": "683512"},
|
||||||
|
{"city": "North Paravur", "district": "Ernakulam", "pincode": "683513"},
|
||||||
|
{"city": "Alangad Town", "district": "Ernakulam", "pincode": "683512"},
|
||||||
|
{"city": "Vadavucode Puthencruz", "district": "Ernakulam", "pincode": "682308"},
|
||||||
|
{"city": "Rayamangalam", "district": "Ernakulam", "pincode": "686692"},
|
||||||
|
{"city": "Edakkattuvayal", "district": "Ernakulam", "pincode": "683565"},
|
||||||
|
{"city": "Varapuzha", "district": "Ernakulam", "pincode": "683517"},
|
||||||
|
{"city": "Pallippuram", "district": "Ernakulam", "pincode": "683513"}
|
||||||
|
]
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
// lib/features/booking/providers/checkout_provider.dart
|
// lib/features/booking/providers/checkout_provider.dart
|
||||||
|
|
||||||
|
import 'dart:convert';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import '../../../core/api/api_endpoints.dart';
|
||||||
import '../../../core/utils/error_utils.dart';
|
import '../../../core/utils/error_utils.dart';
|
||||||
import '../models/booking_models.dart';
|
import '../models/booking_models.dart';
|
||||||
import '../services/booking_service.dart';
|
import '../services/booking_service.dart';
|
||||||
@@ -24,8 +28,11 @@ class CheckoutProvider extends ChangeNotifier {
|
|||||||
// Shipping
|
// Shipping
|
||||||
ShippingDetails? shippingDetails;
|
ShippingDetails? shippingDetails;
|
||||||
|
|
||||||
// Coupon
|
// Coupon / promo
|
||||||
String? couponCode;
|
String? couponCode;
|
||||||
|
double discountAmount = 0.0;
|
||||||
|
String? promoMessage;
|
||||||
|
bool promoApplied = false;
|
||||||
|
|
||||||
// Status
|
// Status
|
||||||
bool loading = false;
|
bool loading = false;
|
||||||
@@ -40,6 +47,9 @@ class CheckoutProvider extends ChangeNotifier {
|
|||||||
cart = [];
|
cart = [];
|
||||||
shippingDetails = null;
|
shippingDetails = null;
|
||||||
couponCode = null;
|
couponCode = null;
|
||||||
|
discountAmount = 0.0;
|
||||||
|
promoMessage = null;
|
||||||
|
promoApplied = false;
|
||||||
paymentId = null;
|
paymentId = null;
|
||||||
error = null;
|
error = null;
|
||||||
loading = true;
|
loading = true;
|
||||||
@@ -65,7 +75,7 @@ class CheckoutProvider extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
double get subtotal => cart.fold(0, (sum, item) => sum + item.subtotal);
|
double get subtotal => cart.fold(0, (sum, item) => sum + item.subtotal);
|
||||||
double get total => subtotal; // expand with discount/tax later
|
double get total => subtotal - discountAmount;
|
||||||
|
|
||||||
bool get hasItems => cart.isNotEmpty;
|
bool get hasItems => cart.isNotEmpty;
|
||||||
|
|
||||||
@@ -95,6 +105,62 @@ class CheckoutProvider extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply a promo code against the backend.
|
||||||
|
Future<bool> applyPromo(String code) async {
|
||||||
|
if (code.trim().isEmpty) return false;
|
||||||
|
loading = true;
|
||||||
|
error = null;
|
||||||
|
notifyListeners();
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final token = prefs.getString('access_token') ?? '';
|
||||||
|
final response = await http.post(
|
||||||
|
Uri.parse('${ApiEndpoints.baseUrl}/bookings/apply-promo/'),
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
},
|
||||||
|
body: jsonEncode({'code': code.trim(), 'event_id': eventId}),
|
||||||
|
);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final data = jsonDecode(response.body);
|
||||||
|
if (data['valid'] == true) {
|
||||||
|
discountAmount = (data['discount_amount'] as num?)?.toDouble() ?? 0.0;
|
||||||
|
couponCode = code.trim();
|
||||||
|
promoMessage = data['message'] as String? ?? 'Promo applied!';
|
||||||
|
promoApplied = true;
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
promoMessage = data['message'] as String? ?? 'Invalid promo code';
|
||||||
|
promoApplied = false;
|
||||||
|
discountAmount = 0.0;
|
||||||
|
couponCode = null;
|
||||||
|
notifyListeners();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
promoMessage = 'Could not apply promo code';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
promoMessage = 'Could not apply promo code';
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove applied promo code.
|
||||||
|
void resetPromo() {
|
||||||
|
discountAmount = 0.0;
|
||||||
|
couponCode = null;
|
||||||
|
promoMessage = null;
|
||||||
|
promoApplied = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
/// Process checkout on backend.
|
/// Process checkout on backend.
|
||||||
Future<Map<String, dynamic>> processCheckout() async {
|
Future<Map<String, dynamic>> processCheckout() async {
|
||||||
loading = true;
|
loading = true;
|
||||||
@@ -139,6 +205,9 @@ class CheckoutProvider extends ChangeNotifier {
|
|||||||
cart = [];
|
cart = [];
|
||||||
shippingDetails = null;
|
shippingDetails = null;
|
||||||
couponCode = null;
|
couponCode = null;
|
||||||
|
discountAmount = 0.0;
|
||||||
|
promoMessage = null;
|
||||||
|
promoApplied = false;
|
||||||
paymentId = null;
|
paymentId = null;
|
||||||
error = null;
|
error = null;
|
||||||
loading = false;
|
loading = false;
|
||||||
|
|||||||
@@ -72,6 +72,11 @@ class EventModel {
|
|||||||
final double? averageRating;
|
final double? averageRating;
|
||||||
final int? reviewCount;
|
final int? reviewCount;
|
||||||
|
|
||||||
|
// Contributor fields (EVT-001)
|
||||||
|
final String? contributorId;
|
||||||
|
final String? contributorName;
|
||||||
|
final String? contributorTier;
|
||||||
|
|
||||||
EventModel({
|
EventModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.name,
|
required this.name,
|
||||||
@@ -97,6 +102,9 @@ class EventModel {
|
|||||||
this.importantInfo = const [],
|
this.importantInfo = const [],
|
||||||
this.averageRating,
|
this.averageRating,
|
||||||
this.reviewCount,
|
this.reviewCount,
|
||||||
|
this.contributorId,
|
||||||
|
this.contributorName,
|
||||||
|
this.contributorTier,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Safely parse a double from backend (may arrive as String or num)
|
/// Safely parse a double from backend (may arrive as String or num)
|
||||||
@@ -156,6 +164,9 @@ class EventModel {
|
|||||||
importantInfo: _parseImportantInfo(j['important_info']),
|
importantInfo: _parseImportantInfo(j['important_info']),
|
||||||
averageRating: (j['average_rating'] as num?)?.toDouble(),
|
averageRating: (j['average_rating'] as num?)?.toDouble(),
|
||||||
reviewCount: (j['review_count'] as num?)?.toInt(),
|
reviewCount: (j['review_count'] as num?)?.toInt(),
|
||||||
|
contributorId: j['contributor_id']?.toString(),
|
||||||
|
contributorName: j['contributor_name'] as String?,
|
||||||
|
contributorTier: j['contributor_tier'] as String?,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,28 @@ class EventsService {
|
|||||||
return EventModel.fromJson(Map<String, dynamic>.from(res));
|
return EventModel.fromJson(Map<String, dynamic>.from(res));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Related events by event_type_id (EVT-002).
|
||||||
|
/// Fetches events with the same category, silently returns [] on failure.
|
||||||
|
Future<List<EventModel>> getEventsByCategory(int eventTypeId, {int limit = 5}) async {
|
||||||
|
try {
|
||||||
|
final res = await _api.post(
|
||||||
|
ApiEndpoints.eventsByCategory,
|
||||||
|
body: {'event_type_id': eventTypeId, 'page_size': limit, 'page': 1},
|
||||||
|
requiresAuth: false,
|
||||||
|
);
|
||||||
|
final results = res['results'] ?? res['events'] ?? res['data'] ?? [];
|
||||||
|
if (results is List) {
|
||||||
|
return results
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map((e) => EventModel.fromJson(e))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// silently fail — related events are non-critical
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
/// Events by month and year for calendar (POST to /events/events-by-month-year/)
|
/// Events by month and year for calendar (POST to /events/events-by-month-year/)
|
||||||
Future<Map<String, dynamic>> getEventsByMonthYear(String month, int year) async {
|
Future<Map<String, dynamic>> getEventsByMonthYear(String month, int year) async {
|
||||||
final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false);
|
final res = await _api.post(ApiEndpoints.eventsByMonth, body: {'month': month, 'year': year}, requiresAuth: false);
|
||||||
|
|||||||
@@ -45,6 +45,33 @@ class GamificationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public contributor profile (any user by userId / email)
|
||||||
|
// GET /v1/gamification/dashboard?user_id={userId}
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
Future<DashboardResponse> getDashboardForUser(String userId) async {
|
||||||
|
final url = '${ApiEndpoints.gamificationDashboard}?user_id=$userId';
|
||||||
|
final res = await _api.post(url, requiresAuth: false);
|
||||||
|
|
||||||
|
final profileJson = res['profile'] as Map<String, dynamic>? ?? {};
|
||||||
|
final rawSubs = res['submissions'] as List? ?? [];
|
||||||
|
final rawAchievements = res['achievements'] as List? ?? [];
|
||||||
|
|
||||||
|
final submissions = rawSubs
|
||||||
|
.map((s) => SubmissionModel.fromJson(Map<String, dynamic>.from(s as Map)))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
final achievements = rawAchievements
|
||||||
|
.map((a) => AchievementBadge.fromJson(Map<String, dynamic>.from(a as Map)))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
return DashboardResponse(
|
||||||
|
profile: UserGamificationProfile.fromJson(profileJson),
|
||||||
|
submissions: submissions,
|
||||||
|
achievements: achievements,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Convenience — returns just the profile (backward-compatible with provider).
|
/// Convenience — returns just the profile (backward-compatible with provider).
|
||||||
Future<UserGamificationProfile> getProfile() async {
|
Future<UserGamificationProfile> getProfile() async {
|
||||||
final dashboard = await getDashboard();
|
final dashboard = await getDashboard();
|
||||||
@@ -152,11 +179,16 @@ class GamificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static const _defaultBadges = [
|
static const _defaultBadges = [
|
||||||
AchievementBadge(id: 'badge-01', title: 'First Submission', description: 'Submitted your first event.', iconName: 'edit', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-01', title: 'First Step', description: 'Submit your first event.', iconName: 'edit', isUnlocked: false, progress: 0.0),
|
||||||
AchievementBadge(id: 'badge-02', title: 'Silver Streak', description: 'Reached Silver tier.', iconName: 'star', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-02', title: 'Silver Streak', description: 'Submit 5 events.', iconName: 'trending_up', isUnlocked: false, progress: 0.0),
|
||||||
AchievementBadge(id: 'badge-03', title: 'Gold Rush', description: 'Reach Gold tier (500 EP).', iconName: 'emoji_events', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-03', title: 'Gold Rush', description: 'Submit 15 events.', iconName: 'emoji_events', isUnlocked: false, progress: 0.0),
|
||||||
AchievementBadge(id: 'badge-04', title: 'Top 10', description: 'Appear in the district leaderboard top 10.', iconName: 'leaderboard', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-04', title: 'Top 10', description: 'Reach top 10 on leaderboard.', iconName: 'leaderboard', isUnlocked: false, progress: 0.0),
|
||||||
AchievementBadge(id: 'badge-05', title: 'Image Pro', description: 'Submit 10 events with 3+ images.', iconName: 'photo_library', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-05', title: 'Image Pro', description: 'Upload 10 event images.', iconName: 'photo_library', isUnlocked: false, progress: 0.0),
|
||||||
AchievementBadge(id: 'badge-06', title: 'Pioneer', description: 'One of the first 100 contributors.', iconName: 'verified', isUnlocked: false, progress: 0.0),
|
AchievementBadge(id: 'badge-06', title: 'Pioneer', description: 'One of the first contributors.', iconName: 'rocket_launch', isUnlocked: false, progress: 0.0),
|
||||||
|
AchievementBadge(id: 'badge-07', title: 'Community Star', description: 'Get 10 events approved.', iconName: 'verified', isUnlocked: false, progress: 0.0),
|
||||||
|
AchievementBadge(id: 'badge-08', title: 'Event Hunter', description: 'Submit 25 events.', iconName: 'event_hunter', isUnlocked: false, progress: 0.0),
|
||||||
|
AchievementBadge(id: 'badge-09', title: 'District Champion', description: 'Rank #1 in your district.', iconName: 'location_on', isUnlocked: false, progress: 0.0),
|
||||||
|
AchievementBadge(id: 'badge-10', title: 'Platinum Achiever', description: 'Earn 1500 EP.', iconName: 'diamond', isUnlocked: false, progress: 0.0),
|
||||||
|
AchievementBadge(id: 'badge-11', title: 'Diamond Legend', description: 'Earn 5000 EP.', iconName: 'workspace_premium', isUnlocked: false, progress: 0.0),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,97 @@
|
|||||||
// lib/features/reviews/widgets/review_summary.dart
|
// lib/features/reviews/widgets/review_summary.dart
|
||||||
|
import 'dart:math' show pi;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../models/review_models.dart';
|
import '../models/review_models.dart';
|
||||||
import 'star_display.dart';
|
|
||||||
|
class _RatingRingPainter extends CustomPainter {
|
||||||
|
final double rating;
|
||||||
|
|
||||||
|
const _RatingRingPainter({required this.rating});
|
||||||
|
|
||||||
|
@override
|
||||||
|
void paint(Canvas canvas, Size size) {
|
||||||
|
final center = Offset(size.width / 2, size.height / 2);
|
||||||
|
final radius = size.width / 2 - 6;
|
||||||
|
|
||||||
|
// Background track
|
||||||
|
canvas.drawArc(
|
||||||
|
Rect.fromCircle(center: center, radius: radius),
|
||||||
|
-pi / 2,
|
||||||
|
2 * pi,
|
||||||
|
false,
|
||||||
|
Paint()
|
||||||
|
..color = Colors.white12
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 7
|
||||||
|
..strokeCap = StrokeCap.round,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filled arc
|
||||||
|
if (rating > 0) {
|
||||||
|
canvas.drawArc(
|
||||||
|
Rect.fromCircle(center: center, radius: radius),
|
||||||
|
-pi / 2,
|
||||||
|
(rating.clamp(0.0, 5.0) / 5.0) * 2 * pi,
|
||||||
|
false,
|
||||||
|
Paint()
|
||||||
|
..color = const Color(0xFFFBBF24)
|
||||||
|
..style = PaintingStyle.stroke
|
||||||
|
..strokeWidth = 7
|
||||||
|
..strokeCap = StrokeCap.round,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool shouldRepaint(_RatingRingPainter old) => old.rating != rating;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RatingRingWidget extends StatelessWidget {
|
||||||
|
final double rating;
|
||||||
|
final int reviewCount;
|
||||||
|
|
||||||
|
const _RatingRingWidget({required this.rating, required this.reviewCount});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 84,
|
||||||
|
height: 84,
|
||||||
|
child: CustomPaint(
|
||||||
|
painter: _RatingRingPainter(rating: rating),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
rating.toStringAsFixed(1),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'/5',
|
||||||
|
style: TextStyle(fontSize: 10, color: Color(0xFF94A3B8)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'$reviewCount ${reviewCount == 1 ? 'review' : 'reviews'}',
|
||||||
|
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ReviewSummary extends StatelessWidget {
|
class ReviewSummary extends StatelessWidget {
|
||||||
final ReviewStatsModel stats;
|
final ReviewStatsModel stats;
|
||||||
@@ -24,27 +114,10 @@ class ReviewSummary extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Left: average rating + stars + count
|
// Left: circular rating ring
|
||||||
Column(
|
_RatingRingWidget(
|
||||||
mainAxisSize: MainAxisSize.min,
|
rating: stats.averageRating,
|
||||||
children: [
|
reviewCount: stats.reviewCount,
|
||||||
Text(
|
|
||||||
stats.averageRating.toStringAsFixed(1),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 40,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Color(0xFF1E293B),
|
|
||||||
height: 1.1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
StarDisplay(rating: stats.averageRating, size: 18),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
'${stats.reviewCount} review${stats.reviewCount == 1 ? '' : 's'}',
|
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFF64748B)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 24),
|
const SizedBox(width: 24),
|
||||||
// Right: distribution bars
|
// Right: distribution bars
|
||||||
|
|||||||
197
lib/features/share/share_rank_card.dart
Normal file
197
lib/features/share/share_rank_card.dart
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
import '../../widgets/tier_avatar_ring.dart';
|
||||||
|
|
||||||
|
class ShareRankCard extends StatefulWidget {
|
||||||
|
final String username;
|
||||||
|
final String tier;
|
||||||
|
final int rank;
|
||||||
|
final int ep;
|
||||||
|
final int rewardPoints;
|
||||||
|
|
||||||
|
const ShareRankCard({
|
||||||
|
super.key,
|
||||||
|
required this.username,
|
||||||
|
required this.tier,
|
||||||
|
required this.rank,
|
||||||
|
required this.ep,
|
||||||
|
this.rewardPoints = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ShareRankCard> createState() => _ShareRankCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareRankCardState extends State<ShareRankCard> {
|
||||||
|
final GlobalKey _boundaryKey = GlobalKey();
|
||||||
|
bool _sharing = false;
|
||||||
|
|
||||||
|
static const _tierGradients = {
|
||||||
|
'Bronze': [Color(0xFF92400E), Color(0xFFD97706)],
|
||||||
|
'Silver': [Color(0xFF475569), Color(0xFF94A3B8)],
|
||||||
|
'Gold': [Color(0xFF92400E), Color(0xFFFBBF24)],
|
||||||
|
'Platinum': [Color(0xFF4C1D95), Color(0xFF8B5CF6)],
|
||||||
|
'Diamond': [Color(0xFF1E3A8A), Color(0xFF60A5FA)],
|
||||||
|
};
|
||||||
|
|
||||||
|
List<Color> get _gradient {
|
||||||
|
return _tierGradients[widget.tier] ?? [const Color(0xFF0F172A), const Color(0xFF1E293B)];
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _share() async {
|
||||||
|
if (_sharing) return;
|
||||||
|
setState(() => _sharing = true);
|
||||||
|
try {
|
||||||
|
final boundary = _boundaryKey.currentContext!.findRenderObject() as RenderRepaintBoundary;
|
||||||
|
final image = await boundary.toImage(pixelRatio: 3.0);
|
||||||
|
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
if (byteData == null) return;
|
||||||
|
final bytes = byteData.buffer.asUint8List();
|
||||||
|
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final file = File('${tempDir.path}/eventify_rank_${widget.username}.png');
|
||||||
|
await file.writeAsBytes(bytes);
|
||||||
|
|
||||||
|
await Share.shareXFiles(
|
||||||
|
[XFile(file.path)],
|
||||||
|
text: 'I\'m ranked #${widget.rank} on Eventify with ${widget.ep} EP! 🏆 #Eventify #Kerala',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text('Could not share rank card')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _sharing = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
RepaintBoundary(
|
||||||
|
key: _boundaryKey,
|
||||||
|
child: Container(
|
||||||
|
width: 320,
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: const LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [Color(0xFF0F172A), Color(0xFF1E293B)],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
// Tier gradient header bar
|
||||||
|
Container(
|
||||||
|
height: 6,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(colors: _gradient),
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Avatar
|
||||||
|
TierAvatarRing(username: widget.username, tier: widget.tier, size: 80),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Username
|
||||||
|
Text(
|
||||||
|
widget.username,
|
||||||
|
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.w800, color: Colors.white),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
// Tier badge
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(colors: _gradient),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
widget.tier.isEmpty ? 'Contributor' : widget.tier,
|
||||||
|
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Stats row
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
_stat('Rank', '#${widget.rank}'),
|
||||||
|
Container(width: 1, height: 40, color: Colors.white12),
|
||||||
|
_stat('EP', '${widget.ep}'),
|
||||||
|
Container(width: 1, height: 40, color: Colors.white12),
|
||||||
|
_stat('RP', '${widget.rewardPoints}'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Branding
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: const [
|
||||||
|
Icon(Icons.bolt, size: 14, color: Color(0xFF3B82F6)),
|
||||||
|
SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'EVENTIFY',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Color(0xFF3B82F6),
|
||||||
|
letterSpacing: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: _sharing ? null : _share,
|
||||||
|
icon: _sharing
|
||||||
|
? const SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.share, size: 18),
|
||||||
|
label: Text(_sharing ? 'Sharing...' : 'Share Rank Card'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF1D4ED8),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _stat(String label, String value) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w800, color: Colors.white),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ class _CheckoutScreenState extends State<CheckoutScreen> {
|
|||||||
final _nameCtrl = TextEditingController();
|
final _nameCtrl = TextEditingController();
|
||||||
final _emailCtrl = TextEditingController();
|
final _emailCtrl = TextEditingController();
|
||||||
final _phoneCtrl = TextEditingController();
|
final _phoneCtrl = TextEditingController();
|
||||||
|
final _promoCtrl = TextEditingController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -77,6 +78,7 @@ class _CheckoutScreenState extends State<CheckoutScreen> {
|
|||||||
_nameCtrl.dispose();
|
_nameCtrl.dispose();
|
||||||
_emailCtrl.dispose();
|
_emailCtrl.dispose();
|
||||||
_phoneCtrl.dispose();
|
_phoneCtrl.dispose();
|
||||||
|
_promoCtrl.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +255,84 @@ class _CheckoutScreenState extends State<CheckoutScreen> {
|
|||||||
_field('Full Name', _nameCtrl, validator: (v) => v!.isEmpty ? 'Required' : null),
|
_field('Full Name', _nameCtrl, validator: (v) => v!.isEmpty ? 'Required' : null),
|
||||||
_field('Email', _emailCtrl, type: TextInputType.emailAddress, 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),
|
_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),
|
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(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import 'package:share_plus/share_plus.dart';
|
|||||||
import '../core/app_decoration.dart';
|
import '../core/app_decoration.dart';
|
||||||
import '../features/gamification/models/gamification_models.dart';
|
import '../features/gamification/models/gamification_models.dart';
|
||||||
import '../features/gamification/providers/gamification_provider.dart';
|
import '../features/gamification/providers/gamification_provider.dart';
|
||||||
|
import '../widgets/glass_card.dart';
|
||||||
import '../widgets/landscape_section_header.dart';
|
import '../widgets/landscape_section_header.dart';
|
||||||
|
import '../widgets/tier_avatar_ring.dart';
|
||||||
|
import '../features/share/share_rank_card.dart';
|
||||||
|
import 'contributor_profile_screen.dart';
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Tier colour map
|
// Tier colour map
|
||||||
@@ -29,12 +33,19 @@ const _tierColors = <ContributorTier, Color>{
|
|||||||
|
|
||||||
// Icon map for achievement badges
|
// Icon map for achievement badges
|
||||||
const _badgeIcons = <String, IconData>{
|
const _badgeIcons = <String, IconData>{
|
||||||
'edit': Icons.edit_outlined,
|
'edit': Icons.edit_outlined,
|
||||||
'star': Icons.star_outline,
|
'star': Icons.star_outline,
|
||||||
'emoji_events': Icons.emoji_events_outlined,
|
'emoji_events': Icons.emoji_events_outlined,
|
||||||
'leaderboard': Icons.leaderboard_outlined,
|
'leaderboard': Icons.leaderboard_outlined,
|
||||||
'photo_library': Icons.photo_library_outlined,
|
'photo_library': Icons.photo_library_outlined,
|
||||||
'verified': Icons.verified_outlined,
|
'verified': Icons.verified_outlined,
|
||||||
|
// ACH-002: icons for expanded badge set (badges 02, 06–11)
|
||||||
|
'trending_up': Icons.trending_up,
|
||||||
|
'rocket_launch': Icons.rocket_launch_outlined,
|
||||||
|
'event_hunter': Icons.search_outlined,
|
||||||
|
'location_on': Icons.location_on_outlined,
|
||||||
|
'diamond': Icons.diamond_outlined,
|
||||||
|
'workspace_premium': Icons.workspace_premium_outlined,
|
||||||
};
|
};
|
||||||
|
|
||||||
// District list for the contribution form
|
// District list for the contribution form
|
||||||
@@ -254,7 +265,56 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
children: [
|
children: [
|
||||||
_epStatCard('Lifetime EP', '${profile?.lifetimeEp ?? 0}', Icons.star, const Color(0xFFF59E0B)),
|
_epStatCard('Lifetime EP', '${profile?.lifetimeEp ?? 0}', Icons.star, const Color(0xFFF59E0B)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_epStatCard('Liquid EP', '${profile?.currentEp ?? 0}', Icons.bolt, const Color(0xFF3B82F6)),
|
// GAM-003 + GAM-004: Liquid EP card with cycle countdown and progress
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF3B82F6).withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: const Color(0xFF3B82F6).withOpacity(0.3)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.bolt, color: Color(0xFF3B82F6), size: 20),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${profile?.currentEp ?? 0}',
|
||||||
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 16),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
const Text('Liquid EP', style: TextStyle(color: Colors.white60, fontSize: 10), textAlign: TextAlign.center),
|
||||||
|
if (provider.currentUserStats?.rewardCycleDays != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Converts in ${provider.currentUserStats!.rewardCycleDays}d',
|
||||||
|
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: Builder(
|
||||||
|
builder: (_) {
|
||||||
|
final days = provider.currentUserStats?.rewardCycleDays ?? 30;
|
||||||
|
final elapsed = (30 - days).clamp(0, 30);
|
||||||
|
final ratio = elapsed / 30;
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
value: ratio,
|
||||||
|
minHeight: 4,
|
||||||
|
backgroundColor: Colors.white12,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
ratio > 0.7 ? const Color(0xFFFBBF24) : const Color(0xFF3B82F6),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_epStatCard('Reward Pts', '${profile?.currentRp ?? 0}', Icons.redeem, const Color(0xFF10B981)),
|
_epStatCard('Reward Pts', '${profile?.currentRp ?? 0}', Icons.redeem, const Color(0xFF10B981)),
|
||||||
],
|
],
|
||||||
@@ -1169,20 +1229,34 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
|
|
||||||
// Badge icon colors
|
// Badge icon colors
|
||||||
final iconColors = <String, Color>{
|
final iconColors = <String, Color>{
|
||||||
'edit': const Color(0xFF3B82F6),
|
'edit': const Color(0xFF3B82F6),
|
||||||
'star': const Color(0xFFF59E0B),
|
'star': const Color(0xFFF59E0B),
|
||||||
'emoji_events': const Color(0xFFF97316),
|
'emoji_events': const Color(0xFFF97316),
|
||||||
'leaderboard': const Color(0xFF8B5CF6),
|
'leaderboard': const Color(0xFF8B5CF6),
|
||||||
'photo_library': const Color(0xFF6B7280),
|
'photo_library': const Color(0xFF6B7280),
|
||||||
'verified': const Color(0xFF10B981),
|
'verified': const Color(0xFF10B981),
|
||||||
|
// ACH-002: colors for expanded badge set
|
||||||
|
'trending_up': const Color(0xFF0EA5E9),
|
||||||
|
'rocket_launch': const Color(0xFFEC4899),
|
||||||
|
'event_hunter': const Color(0xFF64748B),
|
||||||
|
'location_on': const Color(0xFF22C55E),
|
||||||
|
'diamond': const Color(0xFF06B6D4),
|
||||||
|
'workspace_premium': const Color(0xFFE879F9),
|
||||||
};
|
};
|
||||||
final bgColors = <String, Color>{
|
final bgColors = <String, Color>{
|
||||||
'edit': const Color(0xFFDBEAFE),
|
'edit': const Color(0xFFDBEAFE),
|
||||||
'star': const Color(0xFFFEF3C7),
|
'star': const Color(0xFFFEF3C7),
|
||||||
'emoji_events': const Color(0xFFFED7AA),
|
'emoji_events': const Color(0xFFFED7AA),
|
||||||
'leaderboard': const Color(0xFFEDE9FE),
|
'leaderboard': const Color(0xFFEDE9FE),
|
||||||
'photo_library': const Color(0xFFF3F4F6),
|
'photo_library': const Color(0xFFF3F4F6),
|
||||||
'verified': const Color(0xFFD1FAE5),
|
'verified': const Color(0xFFD1FAE5),
|
||||||
|
// ACH-002: backgrounds for expanded badge set
|
||||||
|
'trending_up': const Color(0xFFE0F2FE),
|
||||||
|
'rocket_launch': const Color(0xFFFCE7F3),
|
||||||
|
'event_hunter': const Color(0xFFF1F5F9),
|
||||||
|
'location_on': const Color(0xFFDCFCE7),
|
||||||
|
'diamond': const Color(0xFFCFFAFE),
|
||||||
|
'workspace_premium': const Color(0xFFFAE8FF),
|
||||||
};
|
};
|
||||||
|
|
||||||
final iconColor = isUnlocked ? (iconColors[badge.iconName] ?? const Color(0xFF6B7280)) : const Color(0xFF9CA3AF);
|
final iconColor = isUnlocked ? (iconColors[badge.iconName] ?? const Color(0xFF6B7280)) : const Color(0xFF9CA3AF);
|
||||||
@@ -1520,9 +1594,10 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
Widget _buildContributeTab(BuildContext context, GamificationProvider provider) {
|
Widget _buildContributeTab(BuildContext context, GamificationProvider provider) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final bottomInset = MediaQuery.of(context).padding.bottom;
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
padding: const EdgeInsets.fromLTRB(16, 20, 16, 32),
|
padding: EdgeInsets.fromLTRB(16, 20, 16, 32 + bottomInset),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -2021,6 +2096,26 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
// ── Time period toggle (top-right) + district scroll ──────────────────
|
// ── Time period toggle (top-right) + district scroll ──────────────────
|
||||||
_buildLeaderboardFilters(provider),
|
_buildLeaderboardFilters(provider),
|
||||||
|
|
||||||
|
// LDR-003: Current user stats card at top of leaderboard
|
||||||
|
if (provider.currentUserStats != null)
|
||||||
|
Builder(builder: (context) {
|
||||||
|
final stats = provider.currentUserStats!;
|
||||||
|
return GlassCard(
|
||||||
|
margin: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
_buildStatChip('Rank', '#${stats.rank}', Icons.leaderboard),
|
||||||
|
Container(width: 1, height: 32, color: Colors.white12),
|
||||||
|
_buildStatChip('EP', '${stats.points}', Icons.bolt),
|
||||||
|
Container(width: 1, height: 32, color: Colors.white12),
|
||||||
|
_buildStatChip('Cycle', '${stats.rewardCycleDays}d', Icons.timelapse),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
color: const Color(0xFFFAFBFC),
|
color: const Color(0xFFFAFBFC),
|
||||||
@@ -2183,29 +2278,16 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
return Expanded(
|
return Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Avatar with rank badge overlaid
|
// GAM-006: Avatar with tier ring + rank badge overlaid
|
||||||
Stack(
|
Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
// Avatar circle
|
// TierAvatarRing — tier-coloured glow ring
|
||||||
Container(
|
TierAvatarRing(
|
||||||
width: avatarSize,
|
username: e.username,
|
||||||
height: avatarSize,
|
tier: tierLabel(e.tier),
|
||||||
decoration: BoxDecoration(
|
size: avatarSize,
|
||||||
shape: BoxShape.circle,
|
imageUrl: e.avatarUrl,
|
||||||
color: const Color(0xFFE0F2FE),
|
|
||||||
border: Border.all(color: pillarColors[i], width: 2.5),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
e.username.isNotEmpty ? e.username[0].toUpperCase() : '?',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: i == 1 ? 24 : 18,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
color: pillarColors[i],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
// Rank badge — bottom-right corner of avatar
|
// Rank badge — bottom-right corner of avatar
|
||||||
Positioned(
|
Positioned(
|
||||||
@@ -2267,7 +2349,17 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
final tierColor = _tierColors[entry.tier]!;
|
final tierColor = _tierColors[entry.tier]!;
|
||||||
final isMe = entry.isCurrentUser;
|
final isMe = entry.isCurrentUser;
|
||||||
|
|
||||||
return Container(
|
return GestureDetector(
|
||||||
|
onTap: () => Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => ContributorProfileScreen(
|
||||||
|
contributorId: entry.username,
|
||||||
|
contributorName: entry.username,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isMe ? const Color(0xFFEFF6FF) : Colors.white,
|
color: isMe ? const Color(0xFFEFF6FF) : Colors.white,
|
||||||
border: const Border(bottom: BorderSide(color: Color(0xFFE5E7EB), width: 0.8)),
|
border: const Border(bottom: BorderSide(color: Color(0xFFE5E7EB), width: 0.8)),
|
||||||
@@ -2287,21 +2379,12 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Avatar circle
|
// GAM-006: TierAvatarRing replaces plain avatar circle
|
||||||
Container(
|
TierAvatarRing(
|
||||||
width: 36,
|
username: entry.username,
|
||||||
height: 36,
|
tier: tierLabel(entry.tier),
|
||||||
decoration: BoxDecoration(
|
size: 36,
|
||||||
shape: BoxShape.circle,
|
imageUrl: entry.avatarUrl,
|
||||||
color: const Color(0xFFE0F2FE),
|
|
||||||
border: Border.all(color: tierColor.withOpacity(0.5), width: 1.5),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
entry.username.isNotEmpty ? entry.username[0].toUpperCase() : '?',
|
|
||||||
style: TextStyle(fontWeight: FontWeight.w700, fontSize: 14, color: tierColor),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
// Name
|
// Name
|
||||||
@@ -2360,6 +2443,7 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2386,10 +2470,19 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Share.share(
|
final gam = context.read<GamificationProvider>();
|
||||||
'I\'m ranked #${me.rank} on @EventifyPlus with ${me.lifetimeEp} EP! 🏆 '
|
showDialog(
|
||||||
'Discover & contribute to events near you at eventifyplus.com',
|
context: context,
|
||||||
subject: 'My Eventify.Plus Leaderboard Rank',
|
builder: (_) => Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: ShareRankCard(
|
||||||
|
username: me.username,
|
||||||
|
tier: tierLabel(me.tier),
|
||||||
|
rank: me.rank,
|
||||||
|
ep: me.lifetimeEp,
|
||||||
|
rewardPoints: gam.profile?.currentRp ?? 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -2410,6 +2503,19 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LDR-003: Stat chip helper for current-user leaderboard card
|
||||||
|
Widget _buildStatChip(String label, String value, IconData icon) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: const Color(0xFF94A3B8)),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: Colors.white)),
|
||||||
|
Text(label, style: const TextStyle(fontSize: 10, color: Color(0xFF64748B))),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// TAB 2 — ACHIEVEMENTS
|
// TAB 2 — ACHIEVEMENTS
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -2421,9 +2527,10 @@ class _ContributeScreenState extends State<ContributeScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
final badges = provider.achievements;
|
final badges = provider.achievements;
|
||||||
|
final bottomInset = MediaQuery.of(context).padding.bottom;
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
|
padding: EdgeInsets.fromLTRB(16, 16, 16, 32 + bottomInset),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
271
lib/screens/contributor_profile_screen.dart
Normal file
271
lib/screens/contributor_profile_screen.dart
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
// lib/screens/contributor_profile_screen.dart
|
||||||
|
// CTR-004 — Public contributor profile page.
|
||||||
|
// Shows avatar, tier ring, EP stats, and submission grid for any contributor.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../features/gamification/models/gamification_models.dart';
|
||||||
|
import '../features/gamification/services/gamification_service.dart';
|
||||||
|
import '../widgets/tier_avatar_ring.dart';
|
||||||
|
|
||||||
|
class ContributorProfileScreen extends StatefulWidget {
|
||||||
|
final String contributorId;
|
||||||
|
final String contributorName;
|
||||||
|
|
||||||
|
const ContributorProfileScreen({
|
||||||
|
super.key,
|
||||||
|
required this.contributorId,
|
||||||
|
required this.contributorName,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ContributorProfileScreen> createState() => _ContributorProfileScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContributorProfileScreenState extends State<ContributorProfileScreen> {
|
||||||
|
DashboardResponse? _data;
|
||||||
|
bool _loading = true;
|
||||||
|
String? _error;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_load();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _load() async {
|
||||||
|
try {
|
||||||
|
final data = await GamificationService().getDashboardForUser(widget.contributorId);
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_data = data;
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_error = 'Could not load profile';
|
||||||
|
_loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFF0F172A),
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: const Color(0xFF0F172A),
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
title: Text(
|
||||||
|
widget.contributorName,
|
||||||
|
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
elevation: 0,
|
||||||
|
),
|
||||||
|
body: _loading
|
||||||
|
? const Center(
|
||||||
|
child: CircularProgressIndicator(color: Color(0xFF3B82F6)),
|
||||||
|
)
|
||||||
|
: _error != null
|
||||||
|
? Center(
|
||||||
|
child: Text(
|
||||||
|
_error!,
|
||||||
|
style: const TextStyle(color: Colors.white54),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _buildContent(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildContent() {
|
||||||
|
final profile = _data!.profile;
|
||||||
|
final submissions = _data!.submissions;
|
||||||
|
final tierStr = tierLabel(profile.tier);
|
||||||
|
|
||||||
|
return CustomScrollView(
|
||||||
|
slivers: [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Avatar with tier ring
|
||||||
|
TierAvatarRing(
|
||||||
|
username: widget.contributorName,
|
||||||
|
tier: tierStr,
|
||||||
|
size: 88,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Text(
|
||||||
|
widget.contributorName,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF1E3A8A),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
tierStr,
|
||||||
|
style: const TextStyle(fontSize: 12, color: Color(0xFF93C5FD)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Stats row
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
_statCard('EP', '${profile.currentEp}'),
|
||||||
|
_statCard('Events', '${submissions.length}'),
|
||||||
|
_statCard(
|
||||||
|
'Approved',
|
||||||
|
'${submissions.where((s) => s.status.toUpperCase() == 'APPROVED').length}',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (submissions.isNotEmpty)
|
||||||
|
SliverPadding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 8, 12, 24),
|
||||||
|
sliver: SliverGrid(
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(context, i) => _buildSubmissionTile(submissions[i]),
|
||||||
|
childCount: submissions.length,
|
||||||
|
),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
crossAxisSpacing: 8,
|
||||||
|
mainAxisSpacing: 8,
|
||||||
|
childAspectRatio: 1.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
const SliverToBoxAdapter(
|
||||||
|
child: Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(32),
|
||||||
|
child: Text(
|
||||||
|
'No submissions yet',
|
||||||
|
style: TextStyle(color: Colors.white38),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _statCard(String label, String value) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(fontSize: 12, color: Color(0xFF94A3B8)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSubmissionTile(SubmissionModel s) {
|
||||||
|
final Color statusColor;
|
||||||
|
switch (s.status.toUpperCase()) {
|
||||||
|
case 'APPROVED':
|
||||||
|
statusColor = const Color(0xFF22C55E);
|
||||||
|
break;
|
||||||
|
case 'REJECTED':
|
||||||
|
statusColor = const Color(0xFFEF4444);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
statusColor = const Color(0xFFFBBF24); // PENDING
|
||||||
|
}
|
||||||
|
|
||||||
|
// SubmissionModel.images is List<String>; use first image if present.
|
||||||
|
final String? firstImage = s.images.isNotEmpty ? s.images.first : null;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF1E293B),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
if (firstImage != null && firstImage.isNotEmpty)
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
child: SizedBox.expand(
|
||||||
|
child: Image.network(
|
||||||
|
firstImage,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder: (_, __, ___) => Container(color: const Color(0xFF334155)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 6,
|
||||||
|
right: 6,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: statusColor.withOpacity(0.9),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
s.status,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 9,
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (s.eventName.isNotEmpty)
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.bottomCenter,
|
||||||
|
end: Alignment.topCenter,
|
||||||
|
colors: [Colors.black87, Colors.transparent],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.vertical(bottom: Radius.circular(10)),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
s.eventName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: const TextStyle(fontSize: 10, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,7 +50,7 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
|
|
||||||
// Hero carousel
|
// Hero carousel
|
||||||
final PageController _heroPageController = PageController(viewportFraction: 0.88);
|
final PageController _heroPageController = PageController(viewportFraction: 0.9);
|
||||||
late final ValueNotifier<int> _heroPageNotifier;
|
late final ValueNotifier<int> _heroPageNotifier;
|
||||||
Timer? _autoScrollTimer;
|
Timer? _autoScrollTimer;
|
||||||
|
|
||||||
@@ -453,10 +453,11 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
),
|
),
|
||||||
|
|
||||||
// Floating bottom navigation (always visible)
|
// Floating bottom navigation (always visible)
|
||||||
|
// bottom offset accounts for home indicator on iPhone/Android gesture bar
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 16,
|
left: 16,
|
||||||
right: 16,
|
right: 16,
|
||||||
bottom: 16,
|
bottom: MediaQuery.of(context).padding.bottom + 16,
|
||||||
child: _buildFloatingBottomNav(),
|
child: _buildFloatingBottomNav(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1532,11 +1533,14 @@ class _HomeScreenState extends State<HomeScreen> with SingleTickerProviderStateM
|
|||||||
_selectedDateFilter.isNotEmpty ? 'No events for "$_selectedDateFilter"' : 'No events found',
|
_selectedDateFilter.isNotEmpty ? 'No events for "$_selectedDateFilter"' : 'No events found',
|
||||||
style: const TextStyle(color: Color(0xFF9CA3AF)),
|
style: const TextStyle(color: Color(0xFF9CA3AF)),
|
||||||
))
|
))
|
||||||
: ListView.separated(
|
: PageView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
controller: PageController(viewportFraction: 0.85),
|
||||||
|
physics: const PageScrollPhysics(),
|
||||||
itemCount: _allFilteredByDate.length,
|
itemCount: _allFilteredByDate.length,
|
||||||
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
itemBuilder: (context, index) => Padding(
|
||||||
itemBuilder: (context, index) => _buildTopEventCard(_allFilteredByDate[index]),
|
padding: const EdgeInsets.only(right: 12),
|
||||||
|
child: _buildTopEventCard(_allFilteredByDate[index]),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import '../core/auth/auth_guard.dart';
|
|||||||
import '../core/utils/error_utils.dart';
|
import '../core/utils/error_utils.dart';
|
||||||
import '../core/constants.dart';
|
import '../core/constants.dart';
|
||||||
import '../features/reviews/widgets/review_section.dart';
|
import '../features/reviews/widgets/review_section.dart';
|
||||||
|
import '../widgets/tier_avatar_ring.dart';
|
||||||
|
import 'contributor_profile_screen.dart';
|
||||||
import 'checkout_screen.dart';
|
import 'checkout_screen.dart';
|
||||||
|
|
||||||
class LearnMoreScreen extends StatefulWidget {
|
class LearnMoreScreen extends StatefulWidget {
|
||||||
@@ -59,6 +61,10 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
// Google Map
|
// Google Map
|
||||||
GoogleMapController? _mapController;
|
GoogleMapController? _mapController;
|
||||||
|
|
||||||
|
// Related events (EVT-002)
|
||||||
|
List<EventModel> _relatedEvents = [];
|
||||||
|
bool _loadingRelated = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
@@ -100,6 +106,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
_event = ev;
|
_event = ev;
|
||||||
});
|
});
|
||||||
_startAutoScroll();
|
_startAutoScroll();
|
||||||
|
_loadRelatedEvents();
|
||||||
return; // success
|
return; // success
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('_loadFullDetails attempt ${attempt + 1} failed for event ${widget.eventId}: $e');
|
debugPrint('_loadFullDetails attempt ${attempt + 1} failed for event ${widget.eventId}: $e');
|
||||||
@@ -120,6 +127,7 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _event = ev);
|
setState(() => _event = ev);
|
||||||
_startAutoScroll();
|
_startAutoScroll();
|
||||||
|
_loadRelatedEvents();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() => _error = userFriendlyError(e));
|
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
|
// Carousel helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -441,8 +462,12 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
if (_event!.importantInfo.isEmpty &&
|
if (_event!.importantInfo.isEmpty &&
|
||||||
(_event!.importantInformation ?? '').isNotEmpty)
|
(_event!.importantInformation ?? '').isNotEmpty)
|
||||||
_buildImportantInfoFallback(theme),
|
_buildImportantInfoFallback(theme),
|
||||||
|
// EVT-001: Contributor widget
|
||||||
|
_buildContributorSection(theme),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ReviewSection(eventId: widget.eventId),
|
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 &&
|
if (_event!.importantInfo.isEmpty &&
|
||||||
(_event!.importantInformation ?? '').isNotEmpty)
|
(_event!.importantInformation ?? '').isNotEmpty)
|
||||||
_buildImportantInfoFallback(theme),
|
_buildImportantInfoFallback(theme),
|
||||||
|
// EVT-001: Contributor widget
|
||||||
|
_buildContributorSection(theme),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 0),
|
padding: const EdgeInsets.symmetric(horizontal: 0),
|
||||||
child: ReviewSection(eventId: widget.eventId),
|
child: ReviewSection(eventId: widget.eventId),
|
||||||
),
|
),
|
||||||
|
// EVT-002: Related events horizontal row
|
||||||
|
_buildRelatedEventsSection(theme),
|
||||||
const SizedBox(height: 100),
|
const SizedBox(height: 100),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1335,6 +1364,227 @@ class _LearnMoreScreenState extends State<LearnMoreScreen> {
|
|||||||
return items;
|
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) {
|
Widget _buildImportantInfoFallback(ThemeData theme) {
|
||||||
final parsed = _parseHtmlImportantInfo(_event!.importantInformation!);
|
final parsed = _parseHtmlImportantInfo(_event!.importantInformation!);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import '../core/utils/error_utils.dart';
|
import '../core/utils/error_utils.dart';
|
||||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
@@ -13,11 +16,14 @@ import '../features/events/models/event_models.dart';
|
|||||||
import '../features/gamification/providers/gamification_provider.dart';
|
import '../features/gamification/providers/gamification_provider.dart';
|
||||||
import '../features/gamification/models/gamification_models.dart';
|
import '../features/gamification/models/gamification_models.dart';
|
||||||
import '../widgets/skeleton_loader.dart';
|
import '../widgets/skeleton_loader.dart';
|
||||||
|
import '../widgets/tier_avatar_ring.dart';
|
||||||
import 'learn_more_screen.dart';
|
import 'learn_more_screen.dart';
|
||||||
import 'settings_screen.dart';
|
import 'settings_screen.dart';
|
||||||
|
import '../core/api/api_endpoints.dart';
|
||||||
import '../core/app_decoration.dart';
|
import '../core/app_decoration.dart';
|
||||||
import '../core/constants.dart';
|
import '../core/constants.dart';
|
||||||
import '../widgets/landscape_section_header.dart';
|
import '../widgets/landscape_section_header.dart';
|
||||||
|
import '../features/share/share_rank_card.dart';
|
||||||
|
|
||||||
class ProfileScreen extends StatefulWidget {
|
class ProfileScreen extends StatefulWidget {
|
||||||
const ProfileScreen({Key? key}) : super(key: key);
|
const ProfileScreen({Key? key}) : super(key: key);
|
||||||
@@ -31,10 +37,35 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
String _username = '';
|
String _username = '';
|
||||||
String _email = 'not provided';
|
String _email = 'not provided';
|
||||||
String _profileImage = '';
|
String _profileImage = '';
|
||||||
|
String? _eventifyId;
|
||||||
|
String? _userTier;
|
||||||
|
String? _district;
|
||||||
|
DateTime? _districtChangedAt;
|
||||||
final ImagePicker _picker = ImagePicker();
|
final ImagePicker _picker = ImagePicker();
|
||||||
|
|
||||||
|
// 14 Kerala districts
|
||||||
|
static const List<String> _districts = [
|
||||||
|
'Thiruvananthapuram', 'Kollam', 'Pathanamthitta', 'Alappuzha',
|
||||||
|
'Kottayam', 'Idukki', 'Ernakulam', 'Thrissur', 'Palakkad',
|
||||||
|
'Malappuram', 'Kozhikode', 'Wayanad', 'Kannur', 'Kasaragod',
|
||||||
|
];
|
||||||
|
|
||||||
final EventsService _eventsService = EventsService();
|
final EventsService _eventsService = EventsService();
|
||||||
|
|
||||||
|
// AUTH-005: District change cooldown (183-day lock)
|
||||||
|
bool get _districtLocked {
|
||||||
|
if (_districtChangedAt == null) return false;
|
||||||
|
return DateTime.now().difference(_districtChangedAt!) < const Duration(days: 183);
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _districtNextChange {
|
||||||
|
if (_districtChangedAt == null) return '';
|
||||||
|
final next = _districtChangedAt!.add(const Duration(days: 183));
|
||||||
|
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
|
||||||
|
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||||
|
return '${next.day} ${months[next.month - 1]} ${next.year}';
|
||||||
|
}
|
||||||
|
|
||||||
List<EventModel> _ongoingEvents = [];
|
List<EventModel> _ongoingEvents = [];
|
||||||
List<EventModel> _upcomingEvents = [];
|
List<EventModel> _upcomingEvents = [];
|
||||||
List<EventModel> _pastEvents = [];
|
List<EventModel> _pastEvents = [];
|
||||||
@@ -149,6 +180,21 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
_profileImage =
|
_profileImage =
|
||||||
prefs.getString(profileImageKey) ?? prefs.getString('profileImage') ?? '';
|
prefs.getString(profileImageKey) ?? prefs.getString('profileImage') ?? '';
|
||||||
|
|
||||||
|
// AUTH-003/PROF-001: Eventify ID
|
||||||
|
_eventifyId = prefs.getString('eventify_id');
|
||||||
|
|
||||||
|
// PROF-004 partial: tier for avatar ring
|
||||||
|
_userTier = prefs.getString('user_tier') ?? prefs.getString('level');
|
||||||
|
|
||||||
|
// PROF-002: District
|
||||||
|
_district = prefs.getString('district');
|
||||||
|
|
||||||
|
// AUTH-005: District change cooldown
|
||||||
|
final districtChangedStr = prefs.getString('district_changed_at');
|
||||||
|
if (districtChangedStr != null) {
|
||||||
|
_districtChangedAt = DateTime.tryParse(districtChangedStr);
|
||||||
|
}
|
||||||
|
|
||||||
await _loadEventsForProfile(prefs);
|
await _loadEventsForProfile(prefs);
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
}
|
}
|
||||||
@@ -266,6 +312,8 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
}
|
}
|
||||||
final String path = xfile.path;
|
final String path = xfile.path;
|
||||||
await _saveProfile(_username, _email, path);
|
await _saveProfile(_username, _email, path);
|
||||||
|
// PROF-004: Upload to server on mobile
|
||||||
|
await _uploadProfilePhoto(path);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Image pick error: $e');
|
debugPrint('Image pick error: $e');
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context)
|
||||||
@@ -273,6 +321,77 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PROF-004: Upload profile photo to server
|
||||||
|
Future<void> _uploadProfilePhoto(String filePath) async {
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final token = prefs.getString('access_token') ?? '';
|
||||||
|
if (token.isEmpty) return;
|
||||||
|
final request = http.MultipartRequest(
|
||||||
|
'PATCH',
|
||||||
|
Uri.parse('${ApiEndpoints.baseUrl}/user/update-profile/'),
|
||||||
|
);
|
||||||
|
request.headers['Authorization'] = 'Bearer $token';
|
||||||
|
request.files.add(await http.MultipartFile.fromPath('profile_picture', filePath));
|
||||||
|
final response = await request.send();
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final body = await response.stream.bytesToString();
|
||||||
|
final data = jsonDecode(body) as Map<String, dynamic>;
|
||||||
|
if (data['profile_picture'] != null) {
|
||||||
|
final newUrl = data['profile_picture'].toString();
|
||||||
|
final prefs2 = await SharedPreferences.getInstance();
|
||||||
|
final currentEmail = prefs2.getString('current_email') ?? prefs2.getString('email') ?? '';
|
||||||
|
final profileImageKey = currentEmail.isNotEmpty ? 'profileImage_$currentEmail' : 'profileImage';
|
||||||
|
await prefs2.setString(profileImageKey, newUrl);
|
||||||
|
if (mounted) setState(() => _profileImage = newUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Photo upload error: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PROF-002: Update district via API with cooldown check
|
||||||
|
Future<void> _updateDistrict(String district) async {
|
||||||
|
if (_districtLocked) {
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('District locked until $_districtNextChange')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final token = prefs.getString('access_token') ?? '';
|
||||||
|
final response = await http.patch(
|
||||||
|
Uri.parse('${ApiEndpoints.baseUrl}/user/update-profile/'),
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer $token',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: jsonEncode({'district': district}),
|
||||||
|
);
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
await prefs.setString('district', district);
|
||||||
|
await prefs.setString('district_changed_at', now.toIso8601String());
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_district = district;
|
||||||
|
_districtChangedAt = now;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('District update error: $e');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(SnackBar(content: Text(userFriendlyError(e))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _enterAssetPathDialog() async {
|
Future<void> _enterAssetPathDialog() async {
|
||||||
final ctl = TextEditingController(text: _profileImage);
|
final ctl = TextEditingController(text: _profileImage);
|
||||||
final result = await showDialog<String?>(
|
final result = await showDialog<String?>(
|
||||||
@@ -420,6 +539,87 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
style: theme.textTheme.bodySmall
|
style: theme.textTheme.bodySmall
|
||||||
?.copyWith(color: theme.hintColor),
|
?.copyWith(color: theme.hintColor),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
// PROF-002: District picker
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerLeft,
|
||||||
|
child: Text(
|
||||||
|
'District',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// AUTH-005: Cooldown lock indicator
|
||||||
|
if (_districtLocked)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.lock_outline,
|
||||||
|
size: 14, color: Colors.amber),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'District locked until $_districtNextChange',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12, color: Colors.amber),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// District pill grid
|
||||||
|
StatefulBuilder(
|
||||||
|
builder: (ctx2, setInner) {
|
||||||
|
return Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: _districts.map((d) {
|
||||||
|
final isSelected = _district == d;
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _districtLocked
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
setInner(() {});
|
||||||
|
await _updateDistrict(d);
|
||||||
|
},
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 180),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isSelected
|
||||||
|
? const Color(0xFF3B82F6)
|
||||||
|
: theme.cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: isSelected
|
||||||
|
? const Color(0xFF3B82F6)
|
||||||
|
: theme.dividerColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
d,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: isSelected
|
||||||
|
? Colors.white
|
||||||
|
: theme.textTheme.bodyMedium?.color,
|
||||||
|
fontWeight: isSelected
|
||||||
|
? FontWeight.w600
|
||||||
|
: FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -433,54 +633,22 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────── Avatar builder (reused, with size param) ─────────
|
// ───────── Avatar builder (AUTH-006 / PROF-004: DiceBear via TierAvatarRing) ─────────
|
||||||
|
|
||||||
Widget _buildProfileAvatar({double size = 96}) {
|
Widget _buildProfileAvatar({double size = 96}) {
|
||||||
final path = _profileImage.trim();
|
final path = _profileImage.trim();
|
||||||
|
// Resolve a network-compatible URL: http URLs pass through directly,
|
||||||
|
// file paths and assets fall back to null so DiceBear is used.
|
||||||
|
String? imageUrl;
|
||||||
if (path.startsWith('http')) {
|
if (path.startsWith('http')) {
|
||||||
return ClipOval(
|
imageUrl = path;
|
||||||
child: CachedNetworkImage(
|
|
||||||
imageUrl: path,
|
|
||||||
memCacheWidth: (size * 2).toInt(),
|
|
||||||
memCacheHeight: (size * 2).toInt(),
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
placeholder: (_, __) =>
|
|
||||||
Container(width: size, height: size, color: const Color(0xFFE5E7EB)),
|
|
||||||
errorWidget: (_, __, ___) =>
|
|
||||||
Icon(Icons.person, size: size / 2, color: Colors.grey)));
|
|
||||||
}
|
}
|
||||||
if (kIsWeb) {
|
return TierAvatarRing(
|
||||||
return ClipOval(
|
username: _username.isNotEmpty ? _username : _email,
|
||||||
child: Image.asset(
|
tier: _userTier ?? '',
|
||||||
path.isNotEmpty ? path : 'assets/images/profile.jpg',
|
size: size,
|
||||||
width: size,
|
imageUrl: imageUrl,
|
||||||
height: size,
|
);
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (_, __, ___) =>
|
|
||||||
Icon(Icons.person, size: size / 2, color: Colors.grey)));
|
|
||||||
}
|
|
||||||
if (path.isNotEmpty &&
|
|
||||||
(path.startsWith('/') || path.contains(Platform.pathSeparator))) {
|
|
||||||
final file = File(path);
|
|
||||||
if (file.existsSync()) {
|
|
||||||
return ClipOval(
|
|
||||||
child: Image.file(file,
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (_, __, ___) =>
|
|
||||||
Icon(Icons.person, size: size / 2, color: Colors.grey)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ClipOval(
|
|
||||||
child: Image.asset('assets/images/profile.jpg',
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (_, __, ___) =>
|
|
||||||
Icon(Icons.person, size: size / 2, color: Colors.grey)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ───────── Event list tile (updated styling) ─────────
|
// ───────── Event list tile (updated styling) ─────────
|
||||||
@@ -636,7 +804,34 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(width: 40), // balance
|
// Share rank card button
|
||||||
|
Consumer<GamificationProvider>(
|
||||||
|
builder: (context, gam, _) => GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: ShareRankCard(
|
||||||
|
username: _username,
|
||||||
|
tier: gam.currentUserStats?.level ?? _userTier ?? '',
|
||||||
|
rank: gam.currentUserStats?.rank ?? 0,
|
||||||
|
ep: gam.profile?.currentEp ?? 0,
|
||||||
|
rewardPoints: gam.profile?.currentRp ?? 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white24,
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
|
child: const Icon(Icons.share_outlined, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
'Profile',
|
'Profile',
|
||||||
@@ -927,6 +1122,51 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
// AUTH-003 / PROF-001: Eventify ID badge
|
||||||
|
if (_eventifyId != null && _eventifyId!.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Clipboard.setData(ClipboardData(text: _eventifyId!));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Eventify ID copied'),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.only(top: 2),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF1E3A8A).withOpacity(0.3),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: const Color(0xFF3B82F6).withOpacity(0.5)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.badge_outlined,
|
||||||
|
size: 12, color: Color(0xFF93C5FD)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_eventifyId!,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Color(0xFF93C5FD),
|
||||||
|
fontFamily: 'monospace',
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// Email
|
// Email
|
||||||
@@ -1581,7 +1821,10 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: theme.scaffoldBackgroundColor,
|
backgroundColor: theme.scaffoldBackgroundColor,
|
||||||
// CustomScrollView: only visible event cards are built — no full-tree Column renders
|
// CustomScrollView: only visible event cards are built — no full-tree Column renders
|
||||||
body: CustomScrollView(
|
// SafeArea(bottom: false) — bottom is handled by home screen's floating nav buffer
|
||||||
|
body: SafeArea(
|
||||||
|
bottom: false,
|
||||||
|
child: CustomScrollView(
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
slivers: [
|
slivers: [
|
||||||
// Header gradient + Profile card overlap (same visual as before)
|
// Header gradient + Profile card overlap (same visual as before)
|
||||||
@@ -1667,6 +1910,7 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
const SliverToBoxAdapter(child: SizedBox(height: 32)),
|
const SliverToBoxAdapter(child: SizedBox(height: 32)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1688,7 +1932,56 @@ class _ProfileScreenState extends State<ProfileScreen>
|
|||||||
children: [
|
children: [
|
||||||
_gamStatCard('Lifetime EP', '${p.lifetimeEp}', Icons.star, const Color(0xFFF59E0B), theme),
|
_gamStatCard('Lifetime EP', '${p.lifetimeEp}', Icons.star, const Color(0xFFF59E0B), theme),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
_gamStatCard('Liquid EP', '${p.currentEp}', Icons.bolt, const Color(0xFF3B82F6), theme),
|
// GAM-003 + GAM-004: Liquid EP with cycle countdown and progress bar
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF3B82F6).withOpacity(0.08),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: const Color(0xFF3B82F6).withOpacity(0.2)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.bolt, color: Color(0xFF3B82F6), size: 22),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${p.currentEp}',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w800, fontSize: 16, color: theme.textTheme.bodyLarge?.color),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text('Liquid EP', style: TextStyle(color: theme.hintColor, fontSize: 10), textAlign: TextAlign.center),
|
||||||
|
if (gp.currentUserStats?.rewardCycleDays != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Converts in ${gp.currentUserStats!.rewardCycleDays}d',
|
||||||
|
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8)),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
child: Builder(
|
||||||
|
builder: (_) {
|
||||||
|
final days = gp.currentUserStats?.rewardCycleDays ?? 30;
|
||||||
|
final elapsed = (30 - days).clamp(0, 30);
|
||||||
|
final ratio = elapsed / 30;
|
||||||
|
return LinearProgressIndicator(
|
||||||
|
value: ratio,
|
||||||
|
minHeight: 4,
|
||||||
|
backgroundColor: Colors.white12,
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
ratio > 0.7 ? const Color(0xFFFBBF24) : const Color(0xFF3B82F6),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
_gamStatCard('Reward Pts', '${p.currentRp}', Icons.redeem, const Color(0xFF10B981), theme),
|
_gamStatCard('Reward Pts', '${p.currentRp}', Icons.redeem, const Color(0xFF10B981), theme),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// lib/screens/search_screen.dart
|
// lib/screens/search_screen.dart
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import '../core/utils/error_utils.dart';
|
import '../core/utils/error_utils.dart';
|
||||||
|
|
||||||
// Location packages
|
// Location packages
|
||||||
@@ -46,50 +48,41 @@ class _SearchScreenState extends State<SearchScreen> {
|
|||||||
'Kottayam',
|
'Kottayam',
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Searchable location database – Kerala towns/cities with pincodes.
|
/// Searchable location database – loaded from assets/data/kerala_pincodes.json.
|
||||||
static const List<_LocationItem> _locationDb = [
|
List<_LocationItem> _locationDb = [];
|
||||||
_LocationItem(city: 'Thiruvananthapuram', district: 'Thiruvananthapuram', pincode: '695001'),
|
bool _pinsLoaded = false;
|
||||||
_LocationItem(city: 'Kazhakoottam', district: 'Thiruvananthapuram', pincode: '695582'),
|
|
||||||
_LocationItem(city: 'Neyyattinkara', district: 'Thiruvananthapuram', pincode: '695121'),
|
|
||||||
_LocationItem(city: 'Attingal', district: 'Thiruvananthapuram', pincode: '695101'),
|
|
||||||
_LocationItem(city: 'Kochi', district: 'Ernakulam', pincode: '682001'),
|
|
||||||
_LocationItem(city: 'Ernakulam', district: 'Ernakulam', pincode: '682011'),
|
|
||||||
_LocationItem(city: 'Aluva', district: 'Ernakulam', pincode: '683101'),
|
|
||||||
_LocationItem(city: 'Kakkanad', district: 'Ernakulam', pincode: '682030'),
|
|
||||||
_LocationItem(city: 'Fort Kochi', district: 'Ernakulam', pincode: '682001'),
|
|
||||||
_LocationItem(city: 'Kozhikode', district: 'Kozhikode', pincode: '673001'),
|
|
||||||
_LocationItem(city: 'Feroke', district: 'Kozhikode', pincode: '673631'),
|
|
||||||
_LocationItem(city: 'Kollam', district: 'Kollam', pincode: '691001'),
|
|
||||||
_LocationItem(city: 'Karunagappally', district: 'Kollam', pincode: '690518'),
|
|
||||||
_LocationItem(city: 'Thrissur', district: 'Thrissur', pincode: '680001'),
|
|
||||||
_LocationItem(city: 'Chavakkad', district: 'Thrissur', pincode: '680506'),
|
|
||||||
_LocationItem(city: 'Guruvayoor', district: 'Thrissur', pincode: '680101'),
|
|
||||||
_LocationItem(city: 'Irinjalakuda', district: 'Thrissur', pincode: '680121'),
|
|
||||||
_LocationItem(city: 'Kannur', district: 'Kannur', pincode: '670001'),
|
|
||||||
_LocationItem(city: 'Thalassery', district: 'Kannur', pincode: '670101'),
|
|
||||||
_LocationItem(city: 'Alappuzha', district: 'Alappuzha', pincode: '688001'),
|
|
||||||
_LocationItem(city: 'Cherthala', district: 'Alappuzha', pincode: '688524'),
|
|
||||||
_LocationItem(city: 'Palakkad', district: 'Palakkad', pincode: '678001'),
|
|
||||||
_LocationItem(city: 'Ottapalam', district: 'Palakkad', pincode: '679101'),
|
|
||||||
_LocationItem(city: 'Malappuram', district: 'Malappuram', pincode: '676505'),
|
|
||||||
_LocationItem(city: 'Manjeri', district: 'Malappuram', pincode: '676121'),
|
|
||||||
_LocationItem(city: 'Tirur', district: 'Malappuram', pincode: '676101'),
|
|
||||||
_LocationItem(city: 'Kottayam', district: 'Kottayam', pincode: '686001'),
|
|
||||||
_LocationItem(city: 'Pala', district: 'Kottayam', pincode: '686575'),
|
|
||||||
_LocationItem(city: 'Pathanamthitta', district: 'Pathanamthitta', pincode: '689645'),
|
|
||||||
_LocationItem(city: 'Idukki', district: 'Idukki', pincode: '685602'),
|
|
||||||
_LocationItem(city: 'Munnar', district: 'Idukki', pincode: '685612'),
|
|
||||||
_LocationItem(city: 'Wayanad', district: 'Wayanad', pincode: '673121'),
|
|
||||||
_LocationItem(city: 'Kalpetta', district: 'Wayanad', pincode: '673121'),
|
|
||||||
_LocationItem(city: 'Kasaragod', district: 'Kasaragod', pincode: '671121'),
|
|
||||||
_LocationItem(city: 'Whitefield', district: 'Bengaluru', pincode: '560066'),
|
|
||||||
_LocationItem(city: 'Bengaluru', district: 'Karnataka', pincode: '560001'),
|
|
||||||
];
|
|
||||||
|
|
||||||
List<_LocationItem> _searchResults = [];
|
List<_LocationItem> _searchResults = [];
|
||||||
bool _showSearchResults = false;
|
bool _showSearchResults = false;
|
||||||
bool _loadingLocation = false;
|
bool _loadingLocation = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadKeralaData();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadKeralaData() async {
|
||||||
|
if (_pinsLoaded) return;
|
||||||
|
try {
|
||||||
|
final jsonStr = await rootBundle.loadString('assets/data/kerala_pincodes.json');
|
||||||
|
final List<dynamic> list = jsonDecode(jsonStr);
|
||||||
|
final loaded = list.map((e) => _LocationItem(
|
||||||
|
city: e['city'] as String,
|
||||||
|
district: e['district'] as String?,
|
||||||
|
pincode: e['pincode'] as String?,
|
||||||
|
)).toList();
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_locationDb = loaded;
|
||||||
|
_pinsLoaded = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
// fallback: keep empty list, search won't crash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_ctrl.dispose();
|
_ctrl.dispose();
|
||||||
|
|||||||
96
lib/widgets/eventify_bottom_sheet.dart
Normal file
96
lib/widgets/eventify_bottom_sheet.dart
Normal 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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
lib/widgets/glass_card.dart
Normal file
53
lib/widgets/glass_card.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
117
lib/widgets/tier_avatar_ring.dart
Normal file
117
lib/widgets/tier_avatar_ring.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -641,7 +641,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ dependencies:
|
|||||||
google_maps_flutter: ^2.5.0
|
google_maps_flutter: ^2.5.0
|
||||||
url_launcher: ^6.2.1
|
url_launcher: ^6.2.1
|
||||||
share_plus: ^7.2.1
|
share_plus: ^7.2.1
|
||||||
|
path_provider: ^2.1.0
|
||||||
provider: ^6.1.2
|
provider: ^6.1.2
|
||||||
video_player: ^2.8.1
|
video_player: ^2.8.1
|
||||||
cached_network_image: ^3.3.1
|
cached_network_image: ^3.3.1
|
||||||
@@ -39,6 +40,7 @@ flutter:
|
|||||||
- assets/images/
|
- assets/images/
|
||||||
- assets/icon/hand_stop.svg
|
- assets/icon/hand_stop.svg
|
||||||
- assets/login-bg.mp4
|
- assets/login-bg.mp4
|
||||||
|
- assets/data/kerala_pincodes.json
|
||||||
fonts:
|
fonts:
|
||||||
- family: Gilroy
|
- family: Gilroy
|
||||||
fonts:
|
fonts:
|
||||||
|
|||||||
Reference in New Issue
Block a user